{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":3362,"databundleVersionId":31148,"isSourceIdPinned":false},{"sourceType":"competition","sourceId":5441,"databundleVersionId":38425,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":1003830,"datasetId":550917,"databundleVersionId":1032543},{"sourceType":"datasetVersion","sourceId":791828,"datasetId":119698,"databundleVersionId":813580}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"1121e723","cell_type":"markdown","source":"<div align=\"center\">\n\n<p style=\"font-size:11px;letter-spacing:0.12em;color:gray;text-transform:uppercase;margin-bottom:4px;\">Deep Learning — Practical Assignment</p>\n\n\n\n<table style=\"border-collapse:collapse;margin:auto;font-size:14px;\">\n  <thead>\n    <tr style=\"background:#f5f5f5;\">\n      <th colspan=\"3\" style=\"padding:10px 20px;text-align:left;font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:#888;border-bottom:1px solid #e0e0e0;\">Team Members</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td style=\"padding:7px 16px;color:#aaa;font-size:12px;\">01</td>\n      <td style=\"padding:7px 12px;font-weight:600;\">Abdelaziz Omar Abdelaziz</td>\n      <td style=\"padding:7px 16px;\"><code>ID: 221849</code></td>\n    </tr>\n    <tr style=\"background:#fafafa;\">\n      <td style=\"padding:7px 16px;color:#aaa;font-size:12px;\">02</td>\n      <td style=\"padding:7px 12px;font-weight:600;\">Abdallah Mohamed Abdelaziz</td>\n      <td style=\"padding:7px 16px;\"><code>ID: 220090</code></td>\n    </tr>\n    <tr>\n      <td style=\"padding:7px 16px;color:#aaa;font-size:12px;\">03</td>\n      <td style=\"padding:7px 12px;font-weight:600;\">Emad Alaa Eldin Zaki</td>\n      <td style=\"padding:7px 16px;\"><code>ID: 222689</code></td>\n    </tr>\n    <tr style=\"background:#fafafa;\">\n      <td style=\"padding:7px 16px;color:#aaa;font-size:12px;\">04</td>\n      <td style=\"padding:7px 12px;font-weight:600;\">Mustafa Somaa Mahmoud</td>\n      <td style=\"padding:7px 16px;\"><code>ID: 221751</code></td>\n    </tr>\n    <tr style=\"border-top:1px solid #e0e0e0;\">\n      <td colspan=\"3\" style=\"padding:4px;\"></td>\n    </tr>\n    <tr>\n      <td style=\"padding:6px 16px;color:#aaa;font-size:12px;\">Course</td>\n      <td colspan=\"2\" style=\"padding:6px 12px;\">Deep Learning</td>\n    </tr>\n    <tr style=\"background:#fafafa;\">\n      <td style=\"padding:6px 16px;color:#aaa;font-size:12px;\">Framework</td>\n      <td colspan=\"2\" style=\"padding:6px 12px;\">TensorFlow 2.x / Keras</td>\n    </tr>\n    <tr>\n      <td style=\"padding:6px 16px;color:#aaa;font-size:12px;\">Hardware</td>\n      <td colspan=\"2\" style=\"padding:6px 12px;\">Kaggle — GPU Tesla T4 ×2</td>\n    </tr>\n  </tbody>\n</table>\n\n---\n\n</div>\n\n## Table of Contents\n\n| # | Section | Description |\n|:---:|:---|:---|\n| 1 | Setup & Libraries | Imports, seeds, GPU configuration |\n| **A** | **Part A &mdash; Cats vs Dogs** | Binary image classification |\n| A.1 | Data Extraction | Unpack ZIP, build labeled DataFrame |\n| A.2 | Data Exploration | Class balance, sample images |\n| A.3 | Train / Val / Test Split | Stratified 80 / 10 / 10 |\n| A.4 | Augmentation & Generators | ImageDataGenerator pipeline |\n| A.5 | CNN Architecture | 4-block deep CNN with BatchNorm |\n| A.6 | Training | Callbacks & learning rate schedule |\n| A.7 | Evaluation | Accuracy, loss curves, confusion matrix |\n| A.8 | Prediction Visualization | Sample predictions |\n| **B** | **Part B &mdash; MNIST** | 10-class handwritten digit recognition |\n| B.1 | Load & Explore | Dataset overview |\n| B.2 | Preprocessing | Normalize, reshape, one-hot encode |\n| B.3 | CNN Architecture | 2-block lightweight CNN |\n| B.4 | Training & Evaluation | Full evaluation pipeline |\n| **C** | **Part C &mdash; Fashion MNIST** | 10-class clothing classification |\n| C.1 | Load & Explore | Dataset overview |\n| C.2 | CNN Architecture | 3-block CNN |\n| C.3 | Training & Evaluation | Full evaluation pipeline |\n| **D** | **Conclusion & Comparison** | Cross-dataset summary |\n\n---\n\n> **Datasets used:**\n> - `/kaggle/input/competitions/dogs-vs-cats/train.zip` &mdash; Kaggle Dogs vs Cats (25,000 images)\n> - `/kaggle/input/competitions/dogs-vs-cats/test1.zip` &mdash; Kaggle test set\n> - `keras.datasets.mnist` &mdash; Built-in MNIST (70,000 images)\n> - `keras.datasets.fashion_mnist` &mdash; Built-in Fashion MNIST (70,000 images)\n","metadata":{}},{"id":"003f1a69","cell_type":"markdown","source":"---\n## 1. Setup & Libraries\n\nAll libraries required across the three parts of this notebook are imported in a single cell.\nCentralizing imports makes the notebook easier to audit and avoids hidden dependencies.\n\n**Reproducibility** is ensured by fixing the random seed across Python, NumPy, and TensorFlow.\nWith the same seed, every split, weight initialization, and augmentation sequence is identical across runs.\n\n| Library Group | Libraries | Purpose |\n|:---|:---|:---|\n| Standard | `os`, `zipfile`, `random`, `warnings` | File I/O, ZIP extraction, randomness control |\n| Numerical | `numpy`, `pandas` | Array operations, DataFrame for label management |\n| Visualization | `matplotlib`, `seaborn` | Training curves, confusion matrices, image grids |\n| Image | `PIL` (Pillow) | Opening and resizing image files |\n| Deep Learning | `tensorflow`, `keras` | CNN construction, training, callbacks, evaluation |\n| Evaluation | `sklearn` | Stratified split, confusion matrix, classification report |\n\n![Deep Learning Pipeline](https://i.imgur.com/T82z202.jpeg)\n\n*Figure 1: A typical CNN pipeline — input image passes through convolutional layers, pooling layers, and a fully connected classifier.*\n","metadata":{}},{"id":"1e7123d6","cell_type":"code","source":"# ── Standard ──────────────────────────────────────────────────────────────────\nimport os\nimport zipfile\nimport random\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ── Numerical & Data ──────────────────────────────────────────────────────────\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom matplotlib.image import imread\n\n# ── Visualization ─────────────────────────────────────────────────────────────\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set_style('darkgrid')\n\n# ── Deep Learning ─────────────────────────────────────────────────────────────\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (\n    Conv2D, MaxPooling2D, Dense, Flatten,\n    Dropout, BatchNormalization\n)\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping\n\n# ── Evaluation ────────────────────────────────────────────────────────────────\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\n\n# ── Reproducibility ───────────────────────────────────────────────────────────\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\n# ── Hardware Info ─────────────────────────────────────────────────────────────\nprint(\"TensorFlow version :\", tf.__version__)\ngpus = tf.config.list_physical_devices('GPU')\nprint(\"GPU available      :\", len(gpus) > 0)\nfor g in gpus:\n    print(\"  ->\", g)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:17:52.140189Z","iopub.execute_input":"2026-04-26T22:17:52.141033Z","iopub.status.idle":"2026-04-26T22:18:33.565539Z","shell.execute_reply.started":"2026-04-26T22:17:52.140990Z","shell.execute_reply":"2026-04-26T22:18:33.564685Z"}},"outputs":[],"execution_count":null},{"id":"070165de","cell_type":"markdown","source":"\n\n---\n**Part A &mdash; Cats vs Dogs**\n---\n\n\n\n**Objective:** Train a CNN to distinguish cat photographs from dog photographs\nwith accuracy exceeding 90% on unseen test data.\n\n| Property | Value |\n|:---|:---|\n| Dataset | Kaggle Dogs vs Cats Competition &mdash; 25,000 labeled JPEG images |\n| Task | Binary Classification: Cat = 0 &nbsp; / &nbsp; Dog = 1 |\n| Input resolution | 128 x 128 pixels, RGB (3 channels) |\n| Target accuracy | > 90% on held-out test set |\n\n**Why this task is challenging:**\nUnlike MNIST or Fashion MNIST, these are real-world photographs with significant\nvariation in lighting, background, pose, scale, and image quality.\nThe model must learn robust visual representations that generalize across all these variations.\n\n![CNN Layers](https://i.imgur.com/2JlmaU3.jpeg)\n\n*Figure A.0: A neural network learns hierarchical representations.\nEarly layers detect edges and gradients; deeper layers recognize semantic structures\nsuch as ears, snouts, and body shapes that distinguish cats from dogs.*\n","metadata":{}},{"id":"2a648d62","cell_type":"markdown","source":"### A.1 Data Extraction from ZIP\n\nThe Kaggle Dogs vs Cats dataset is distributed as a compressed ZIP archive.\nWe extract it to the Kaggle working directory and construct a labeled DataFrame\nby parsing the filename prefix of each image.\n\n**Why filename-based labeling?**\nThe dataset follows a strict naming convention: `cat.N.jpg` and `dog.N.jpg`.\nParsing the prefix is deterministic and error-free, without requiring any\nfolder structure or external metadata file.\n","metadata":{}},{"id":"129248d4","cell_type":"code","source":"WORKING   = \"/kaggle/working/\"\nTRAIN_ZIP = \"/kaggle/input/competitions/dogs-vs-cats/train.zip\"\nTEST_ZIP  = \"/kaggle/input/competitions/dogs-vs-cats/test1.zip\"\n\nprint(\"Extracting training archive...\")\nwith zipfile.ZipFile(TRAIN_ZIP, 'r') as z:\n    z.extractall(WORKING)\nprint(\"  train/ extracted successfully.\")\n\nprint(\"Extracting test archive...\")\nwith zipfile.ZipFile(TEST_ZIP, 'r') as z:\n    z.extractall(WORKING)\nprint(\"  test1/ extracted successfully.\")\n\n# ── Build labeled DataFrame ────────────────────────────────────────\nIMAGE_DIR = os.path.join(WORKING, \"train\")\nfilenames = os.listdir(IMAGE_DIR)\nlabels    = [f.split(\".\")[0] for f in filenames]   # \"cat\" or \"dog\"\n\ndata = pd.DataFrame({\"filename\": filenames, \"label\": labels})\n\nprint(f\"\\nTotal images : {len(data):,}\")\nprint(\"\\nClass distribution:\")\nprint(data['label'].value_counts().to_string())\nprint()\ndata.head(8)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:33.567037Z","iopub.execute_input":"2026-04-26T22:18:33.567602Z","iopub.status.idle":"2026-04-26T22:18:44.231973Z","shell.execute_reply.started":"2026-04-26T22:18:33.567576Z","shell.execute_reply":"2026-04-26T22:18:44.230960Z"}},"outputs":[],"execution_count":null},{"id":"d48406dd","cell_type":"markdown","source":"### A.2 Data Exploration\n\nBefore any model is built, we examine the dataset to understand:\n\n1. **Class balance** &mdash; are Cat and Dog counts equal?\n2. **Visual diversity** &mdash; do images vary in size, angle, and background?\n\n**Key observations:**\n- The dataset is perfectly balanced: 12,500 cats and 12,500 dogs.\n- Images vary significantly in resolution, aspect ratio, background, and pose.\n- Some images contain multiple animals, partial views, or cluttered backgrounds.\n- These factors motivate the use of data augmentation during training.\n","metadata":{}},{"id":"9ab0c8d6","cell_type":"code","source":"# ── Sample Dog images ─────────────────────────────────────────────\nplt.figure(figsize=(20, 3))\nplt.suptitle(\"Sample Dog Images from Training Set\",\n             fontsize=13, fontweight='bold', y=1.05)\nfor i in range(10):\n    plt.subplot(1, 10, i + 1)\n    img = imread(os.path.join(IMAGE_DIR, f\"dog.{i}.jpg\"))\n    plt.imshow(img)\n    plt.title(\"Dog\", fontsize=9, color='#e74c3c')\n    plt.axis('off')\nplt.tight_layout()\nplt.show()\n\n# ── Sample Cat images ─────────────────────────────────────────────\nplt.figure(figsize=(20, 3))\nplt.suptitle(\"Sample Cat Images from Training Set\",\n             fontsize=13, fontweight='bold', y=1.05)\nfor i in range(10):\n    plt.subplot(1, 10, i + 1)\n    img = imread(os.path.join(IMAGE_DIR, f\"cat.{i}.jpg\"))\n    plt.imshow(img)\n    plt.title(\"Cat\", fontsize=9, color='#3498db')\n    plt.axis('off')\nplt.tight_layout()\nplt.show()\n\n# ── Class distribution chart ──────────────────────────────────────\nfig, ax = plt.subplots(figsize=(7, 4), dpi=130)\ncounts = data['label'].value_counts()\nsns.barplot(x=counts.index, y=counts.values,\n            palette=['#3498db', '#e74c3c'], width=0.35, ax=ax)\nax.set_title(\"Class Distribution\", fontsize=12, fontweight='bold')\nax.set_xlabel(\"Class\"); ax.set_ylabel(\"Number of Images\")\nfor bar in ax.patches:\n    ax.text(bar.get_x() + bar.get_width() / 2,\n            bar.get_height() + 80,\n            f\"{int(bar.get_height()):,}\",\n            ha='center', fontsize=11, fontweight='bold')\nax.set_ylim(0, 14500)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:44.233047Z","iopub.execute_input":"2026-04-26T22:18:44.233367Z","iopub.status.idle":"2026-04-26T22:18:45.871682Z","shell.execute_reply.started":"2026-04-26T22:18:44.233335Z","shell.execute_reply":"2026-04-26T22:18:45.870989Z"}},"outputs":[],"execution_count":null},{"id":"1d95eba6","cell_type":"markdown","source":"### A.3 Train / Validation / Test Split\n\nThe dataset is divided into three non-overlapping subsets using **stratified sampling**.\nStratification preserves the 50/50 class balance in every subset.\n\n| Subset | Ratio | Images | Purpose |\n|:---|:---:|:---:|:---|\n| Training | 80% | 20,000 | Update model weights via backpropagation |\n| Validation | 10% | 2,500 | Monitor generalization during training |\n| Test | 10% | 2,500 | Final unbiased evaluation &mdash; never seen during training |\n\n**Why a separate test set?**\nThe validation set is used to make decisions during training (early stopping, LR reduction),\nso the model indirectly adapts to it. The test set is the only truly unbiased performance measure.\n","metadata":{}},{"id":"c488f8a8","cell_type":"code","source":"labels_col = data['label']\n\n# Step 1: Training vs (Validation + Test)\nX_train, X_temp = train_test_split(\n    data, test_size=0.2, stratify=labels_col, random_state=SEED\n)\n\n# Step 2: Validation vs Test\nX_val, X_test = train_test_split(\n    X_temp, test_size=0.5,\n    stratify=X_temp['label'], random_state=SEED\n)\n\nprint(f\"Training set   : {len(X_train):>6,} images  ({len(X_train)/len(data)*100:.0f}%)\")\nprint(f\"Validation set : {len(X_val):>6,} images  ({len(X_val)/len(data)*100:.0f}%)\")\nprint(f\"Test set       : {len(X_test):>6,} images  ({len(X_test)/len(data)*100:.0f}%)\")\n\n# ── Class balance per split ────────────────────────────────────────\nfig, axes = plt.subplots(1, 3, figsize=(16, 5), dpi=130)\nfig.suptitle(\"Class Distribution per Split\", fontsize=13, fontweight='bold')\n\nfor ax, (df, title) in zip(axes, [\n    (X_train, \"Training (80%)\"),\n    (X_val,   \"Validation (10%)\"),\n    (X_test,  \"Test (10%)\")\n]):\n    counts = df['label'].value_counts()\n    sns.barplot(x=counts.index, y=counts.values,\n                palette=['#3498db', '#e74c3c'], width=0.35, ax=ax)\n    ax.set_title(title, fontsize=12, fontweight='bold')\n    ax.set_xlabel(\"Class\"); ax.set_ylabel(\"Images\")\n    for bar in ax.patches:\n        ax.text(bar.get_x() + bar.get_width() / 2,\n                bar.get_height() + 30,\n                f\"{int(bar.get_height()):,}\",\n                ha='center', fontsize=10, fontweight='bold')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:45.873396Z","iopub.execute_input":"2026-04-26T22:18:45.873837Z","iopub.status.idle":"2026-04-26T22:18:46.276600Z","shell.execute_reply.started":"2026-04-26T22:18:45.873814Z","shell.execute_reply":"2026-04-26T22:18:46.275988Z"}},"outputs":[],"execution_count":null},{"id":"b66de2e2","cell_type":"markdown","source":"### A.4 Data Augmentation & Generators\n\n**What is Data Augmentation?**\nAugmentation applies random transformations to training images before each epoch,\npreventing memorization and teaching the model to be invariant to irrelevant variations.\n\n| Transform | Value | Justification |\n|:---|:---:|:---|\n| `rotation_range` | 15 deg | Cameras are not always perfectly level |\n| `horizontal_flip` | True | Cats and dogs are bilaterally symmetric |\n| `zoom_range` | 0.2 | Subjects photographed at different distances |\n| `width_shift_range` | 0.1 | Subject not always centered horizontally |\n| `height_shift_range` | 0.1 | Subject not always centered vertically |\n| `shear_range` | 0.1 | Perspective distortion from camera angle |\n| `fill_mode` | reflect | Fills border pixels naturally |\n\nValidation and test generators use **normalization only** &mdash; no augmentation.\n\n**Why `flow_from_dataframe`?**\nIt accepts an explicit filename-to-label mapping, giving precise control over splits\nand working correctly on Kaggle's read-only filesystem.\n\n![Data Augmentation Example](https://i.imgur.com/Kw0GhMi.jpeg)\n\n*Figure A.4: Common data augmentation operations applied to training images.\nEach epoch the model sees a different random transformation of the same image.*\n","metadata":{}},{"id":"7577b1b4","cell_type":"code","source":"IMAGE_SIZE    = 128\nBATCH_SIZE    = 32\nIMAGE_CHANNEL = 3\n\n# ── Training generator: normalize + augment ────────────────────────\ntrain_datagen = ImageDataGenerator(\n    rescale            = 1.0 / 255,\n    rotation_range     = 15,\n    horizontal_flip    = True,\n    zoom_range         = 0.2,\n    shear_range        = 0.1,\n    fill_mode          = 'reflect',\n    width_shift_range  = 0.1,\n    height_shift_range = 0.1\n)\n\n# ── Evaluation generator: normalize only ──────────────────────────\neval_datagen = ImageDataGenerator(rescale=1.0 / 255)\n\n# ── Flow from DataFrame ───────────────────────────────────────────\ntrain_generator = train_datagen.flow_from_dataframe(\n    X_train,\n    directory   = IMAGE_DIR,\n    x_col       = 'filename',\n    y_col       = 'label',\n    batch_size  = BATCH_SIZE,\n    target_size = (IMAGE_SIZE, IMAGE_SIZE),\n    class_mode  = 'binary'\n)\n\nval_generator = eval_datagen.flow_from_dataframe(\n    X_val,\n    directory   = IMAGE_DIR,\n    x_col       = 'filename',\n    y_col       = 'label',\n    batch_size  = BATCH_SIZE,\n    target_size = (IMAGE_SIZE, IMAGE_SIZE),\n    class_mode  = 'binary',\n    shuffle     = False\n)\n\ntest_generator = eval_datagen.flow_from_dataframe(\n    X_test,\n    directory   = IMAGE_DIR,\n    x_col       = 'filename',\n    y_col       = 'label',\n    batch_size  = BATCH_SIZE,\n    target_size = (IMAGE_SIZE, IMAGE_SIZE),\n    class_mode  = 'binary',\n    shuffle     = False\n)\n\nprint(f\"Class indices      : {train_generator.class_indices}\")\nprint(f\"Training batches   : {len(train_generator)}\")\nprint(f\"Validation batches : {len(val_generator)}\")\nprint(f\"Test batches       : {len(test_generator)}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:46.277379Z","iopub.execute_input":"2026-04-26T22:18:46.277765Z","iopub.status.idle":"2026-04-26T22:18:46.475519Z","shell.execute_reply.started":"2026-04-26T22:18:46.277742Z","shell.execute_reply":"2026-04-26T22:18:46.474935Z"}},"outputs":[],"execution_count":null},{"id":"ceebe85a","cell_type":"code","source":"# ── Visualize augmented samples ────────────────────────────────────\nsample_imgs, sample_labels = next(train_generator)\n\nfig, axes = plt.subplots(2, 5, figsize=(16, 7))\nfig.suptitle(\"Augmented Training Samples (as seen by the model each epoch)\",\n             fontsize=12, fontweight='bold')\n\nfor idx in range(10):\n    ax    = axes[idx // 5][idx % 5]\n    label = \"Dog\" if int(sample_labels[idx]) == 1 else \"Cat\"\n    color = '#e74c3c' if label == \"Dog\" else '#3498db'\n    ax.imshow(sample_imgs[idx])\n    ax.set_title(label, color=color, fontsize=10, fontweight='bold')\n    ax.axis('off')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:46.476397Z","iopub.execute_input":"2026-04-26T22:18:46.476703Z","iopub.status.idle":"2026-04-26T22:18:47.543903Z","shell.execute_reply.started":"2026-04-26T22:18:46.476680Z","shell.execute_reply":"2026-04-26T22:18:47.543016Z"}},"outputs":[],"execution_count":null},{"id":"ded7a84a","cell_type":"markdown","source":"### A.5 CNN Architecture\n\nThe model uses four convolutional blocks followed by a fully connected classification head.\n\n```\nInput: 128 x 128 x 3  (RGB image)\n  +---------------------------------------------------------------+\n  |  Block 1 -- Conv2D(32)  + BatchNorm + MaxPool(2) + Drop(0.2)  |  Output: 63 x 63 x 32\n  |  Block 2 -- Conv2D(64)  + BatchNorm + MaxPool(2) + Drop(0.2)  |  Output: 30 x 30 x 64\n  |  Block 3 -- Conv2D(128) + BatchNorm + MaxPool(2) + Drop(0.2)  |  Output: 14 x 14 x 128\n  |  Block 4 -- Conv2D(256) + BatchNorm + MaxPool(2) + Drop(0.2)  |  Output:  6 x  6 x 256\n  |  Head    -- Flatten + Dense(512) + BatchNorm + Dropout(0.2)   |\n  |  Output  -- Dense(1) + Sigmoid                                 |  Probability in [0, 1]\n  +---------------------------------------------------------------+\n```\n\n| Component | Choice | Reason |\n|:---|:---|:---|\n| Filter progression | 32 to 64 to 128 to 256 | Deeper layers need more filters for complex patterns |\n| BatchNormalization | After every Conv layer | Stabilizes activations, speeds convergence |\n| MaxPooling2D | After every block | Reduces spatial dimensions, builds translation invariance |\n| Dropout(0.2) | After every block | Prevents co-adaptation of feature detectors |\n| Dense(1) + Sigmoid | Output layer | Binary output: probability < 0.5 = Cat, >= 0.5 = Dog |\n| binary_crossentropy | Loss function | Correct loss for single-unit sigmoid binary classification |\n\n![CNN Feature Maps](https://i.imgur.com/66cr55x.jpeg)\n\n*Figure A.5: Feature maps in a CNN become smaller spatially but richer in learned representations as depth increases.*\n","metadata":{}},{"id":"48c4e093","cell_type":"code","source":"model = Sequential([\n    # Block 1 -- low-level features: edges, color boundaries\n    Conv2D(32, (3, 3), activation='relu',\n           input_shape=(IMAGE_SIZE, IMAGE_SIZE, IMAGE_CHANNEL)),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    # Block 2 -- mid-level features: textures, fur patterns\n    Conv2D(64, (3, 3), activation='relu'),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    # Block 3 -- high-level features: shapes, object parts\n    Conv2D(128, (3, 3), activation='relu'),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    # Block 4 -- semantic features: ears, snouts, body silhouettes\n    Conv2D(256, (3, 3), activation='relu'),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    # Classification head\n    Flatten(),\n    Dense(512, activation='relu'),\n    BatchNormalization(),\n    Dropout(0.2),\n\n    # Binary output -- Sigmoid maps to [0, 1]\n    Dense(1, activation='sigmoid')\n], name=\"CatsVsDogs_CNN\")\n\nmodel.compile(\n    optimizer = 'adam',\n    loss      = 'binary_crossentropy',\n    metrics   = ['accuracy']\n)\n\nmodel.summary()\nprint(f\"\\nTotal parameters: {model.count_params():,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:47.545077Z","iopub.execute_input":"2026-04-26T22:18:47.545410Z","iopub.status.idle":"2026-04-26T22:18:49.972576Z","shell.execute_reply.started":"2026-04-26T22:18:47.545385Z","shell.execute_reply":"2026-04-26T22:18:49.971951Z"}},"outputs":[],"execution_count":null},{"id":"e60810f8","cell_type":"markdown","source":"### A.6 Training\n\n| Callback | Monitor | Trigger | Action |\n|:---|:---|:---|:---|\n| ReduceLROnPlateau | val_accuracy | No improvement for 2 epochs | Multiply learning rate by 0.5 |\n| EarlyStopping | val_loss | No improvement for 3 epochs | Stop; restore best weights |\n\n**Why reduce the learning rate?**\nWhen the model stops improving, smaller updates allow finer descent into the loss landscape.\n\n**Why early stopping?**\nHalts training automatically when validation loss plateaus, protecting against overfitting.\n`restore_best_weights=True` ensures the final model corresponds to the best validation epoch.\n","metadata":{}},{"id":"64cd90d9","cell_type":"code","source":"lr_reduction = ReduceLROnPlateau(\n    monitor  = 'val_accuracy',\n    patience = 2,\n    factor   = 0.5,\n    min_lr   = 0.00001,\n    verbose  = 1\n)\n\nearly_stop = EarlyStopping(\n    monitor              = 'val_loss',\n    patience             = 3,\n    restore_best_weights = True,\n    verbose              = 1\n)\n\nprint(\"Starting training -- Cats vs Dogs CNN...\")\nprint(f\"  Epochs (max) : 30  |  Batch size: {BATCH_SIZE}  |  Image size: {IMAGE_SIZE}x{IMAGE_SIZE}\")\nprint()\n\nhistory_cvd = model.fit(\n    train_generator,\n    validation_data = val_generator,\n    callbacks       = [early_stop, lr_reduction],\n    epochs          = 30,\n    verbose         = 1\n)\nprint(\"\\nTraining complete.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:18:49.973373Z","iopub.execute_input":"2026-04-26T22:18:49.973656Z","iopub.status.idle":"2026-04-26T22:58:59.013101Z","shell.execute_reply.started":"2026-04-26T22:18:49.973635Z","shell.execute_reply":"2026-04-26T22:58:59.012487Z"}},"outputs":[],"execution_count":null},{"id":"f83d8861","cell_type":"markdown","source":"### A.7 Evaluation & Confusion Matrix\n\n**Training curves** show how loss and accuracy evolve across epochs.\nA healthy run shows train and validation curves close together without a large gap.\n\n**Confusion matrix** decomposes accuracy into per-class counts:\n- Diagonal cells: correct predictions\n- Off-diagonal cells: misclassifications\n\nThe **Classification Report** adds Precision, Recall, and F1-score per class.\n","metadata":{}},{"id":"a1eab274","cell_type":"code","source":"# ── Training curves ───────────────────────────────────────────────\nerror = pd.DataFrame(history_cvd.history)\n\nfig, axes = plt.subplots(1, 2, figsize=(16, 5), dpi=150)\nfig.suptitle(\"Cats vs Dogs -- Training History\", fontsize=13, fontweight='bold')\n\naxes[0].plot(error['loss'],     lw=2.5, label='Train Loss',      color='#3498db')\naxes[0].plot(error['val_loss'], lw=2.5, label='Validation Loss', color='#e74c3c', ls='--')\naxes[0].set_title(\"Loss per Epoch\"); axes[0].set_xlabel(\"Epoch\"); axes[0].set_ylabel(\"Loss\")\naxes[0].legend(); axes[0].grid(alpha=0.3)\n\naxes[1].plot(error['accuracy'],     lw=2.5, label='Train Accuracy',      color='#3498db')\naxes[1].plot(error['val_accuracy'], lw=2.5, label='Validation Accuracy', color='#e74c3c', ls='--')\naxes[1].set_title(\"Accuracy per Epoch\"); axes[1].set_xlabel(\"Epoch\"); axes[1].set_ylabel(\"Accuracy\")\naxes[1].legend(); axes[1].grid(alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n\nprint(\"\\nFinal Evaluation:\")\nprint(\"-\" * 50)\nfor gen, name in [\n    (train_generator, \"Training\"),\n    (val_generator,   \"Validation\"),\n    (test_generator,  \"Test\")\n]:\n    loss, acc = model.evaluate(gen, batch_size=BATCH_SIZE, verbose=0)\n    print(f\"  {name:<14}  Accuracy: {acc*100:.2f}%   Loss: {loss:.4f}\")\nprint(\"-\" * 50)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T22:58:59.014129Z","iopub.execute_input":"2026-04-26T22:58:59.014500Z","iopub.status.idle":"2026-04-26T23:00:36.460776Z","shell.execute_reply.started":"2026-04-26T22:58:59.014466Z","shell.execute_reply":"2026-04-26T23:00:36.460068Z"}},"outputs":[],"execution_count":null},{"id":"e02fc9a2","cell_type":"code","source":"# ── Confusion matrix ──────────────────────────────────────────────\nresult = model.predict(test_generator, batch_size=BATCH_SIZE, verbose=0)\ny_pred = (result.flatten() > 0.5).astype(int)\ny_true = test_generator.labels\nclass_labels = ['Cat', 'Dog']\n\ncm = confusion_matrix(y_true, y_pred)\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5), dpi=140)\nfig.suptitle(\"Test Set Evaluation -- Cats vs Dogs\", fontsize=13, fontweight='bold')\n\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues',\n            xticklabels=class_labels, yticklabels=class_labels,\n            linewidths=0.5, ax=axes[0], cbar=False, annot_kws={\"size\": 14})\naxes[0].set_title(\"Confusion Matrix (Counts)\"); axes[0].set_xlabel(\"Predicted\"); axes[0].set_ylabel(\"True\")\n\ncm_pct = cm.astype(float) / cm.sum(axis=1, keepdims=True) * 100\nsns.heatmap(cm_pct, annot=True, fmt='.1f', cmap='Blues',\n            xticklabels=class_labels, yticklabels=class_labels,\n            linewidths=0.5, ax=axes[1], cbar=False, annot_kws={\"size\": 14})\naxes[1].set_title(\"Confusion Matrix (Row %)\"); axes[1].set_xlabel(\"Predicted\"); axes[1].set_ylabel(\"True\")\n\nplt.tight_layout()\nplt.show()\n\nprint(\"\\nClassification Report:\")\nprint(classification_report(y_true, y_pred, target_names=class_labels))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:36.463053Z","iopub.execute_input":"2026-04-26T23:00:36.463376Z","iopub.status.idle":"2026-04-26T23:00:41.003470Z","shell.execute_reply.started":"2026-04-26T23:00:36.463352Z","shell.execute_reply":"2026-04-26T23:00:41.002526Z"}},"outputs":[],"execution_count":null},{"id":"884cb945","cell_type":"markdown","source":"### A.8 Prediction Visualization\n\nSample test images displayed with true and predicted labels.\nGreen = correct prediction. Red = incorrect prediction.\n","metadata":{}},{"id":"b2e982e3","cell_type":"code","source":"label_map    = {0: 'Cat', 1: 'Dog'}\nX_test_reset = X_test.reset_index(drop=True)\n\nfig, axes = plt.subplots(2, 5, figsize=(18, 8))\nfig.suptitle(\"Sample Test Predictions  --  green = correct  |  red = incorrect\",\n             fontsize=12, fontweight='bold')\n\nfor idx in range(10):\n    ax    = axes[idx // 5][idx % 5]\n    fname = X_test_reset.iloc[idx]['filename']\n    true  = X_test_reset.iloc[idx]['label'].capitalize()\n    pred  = label_map[y_pred[idx]]\n    color = '#2ecc71' if pred.lower() == true.lower() else '#e74c3c'\n\n    try:\n        img = Image.open(os.path.join(IMAGE_DIR, fname)).resize((200, 200))\n        ax.imshow(img)\n    except Exception:\n        ax.set_facecolor('#eeeeee')\n\n    ax.set_title(f\"True: {true}  |  Pred: {pred}\", color=color, fontsize=9, fontweight='bold')\n    ax.axis('off')\n\nplt.tight_layout()\nplt.show()\n\nmodel.save(\"cats_dogs_model.h5\")\nprint(\"Model saved: cats_dogs_model.h5\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:41.004521Z","iopub.execute_input":"2026-04-26T23:00:41.004831Z","iopub.status.idle":"2026-04-26T23:00:42.311479Z","shell.execute_reply.started":"2026-04-26T23:00:41.004808Z","shell.execute_reply":"2026-04-26T23:00:42.310695Z"}},"outputs":[],"execution_count":null},{"id":"6a6a35ce","cell_type":"markdown","source":"\n---\n**Part B &mdash; MNIST**\n---\n\n| Property | Value |\n|:---|:---|\n| Dataset | MNIST -- 70,000 grayscale images (60,000 train / 10,000 test) |\n| Source | Built into Keras -- no external download required |\n| Image size | 28 x 28 pixels, 1 channel (grayscale) |\n| Task | 10-class classification |\n\n**Why is MNIST simpler than Cats vs Dogs?**\nMNIST images are small (28x28), centered, uniform-background, and free of noise.\nA lightweight 2-block CNN can achieve above 99% accuracy in minutes.\n\n","metadata":{}},{"id":"7e2579ca","cell_type":"markdown","source":"### B.1 Load & Explore","metadata":{}},{"id":"e574528d","cell_type":"code","source":"(X_train_m, y_train_m), (X_test_m, y_test_m) = keras.datasets.mnist.load_data()\n\nprint(\"MNIST dataset loaded from keras.datasets.\")\nprint(f\"  Training images : {X_train_m.shape}\")\nprint(f\"  Test images     : {X_test_m.shape}\")\nprint(f\"  Pixel range     : [{X_train_m.min()}, {X_train_m.max()}]\")\nprint(f\"  Classes         : {np.unique(y_train_m)}\")\n\nfig, axes = plt.subplots(2, 10, figsize=(16, 4))\nfig.suptitle(\"MNIST -- Two Samples per Digit Class\", fontsize=12, fontweight='bold')\nfor digit in range(10):\n    for row, offset in enumerate([0, 5]):\n        idx = np.where(y_train_m == digit)[0][offset]\n        axes[row][digit].imshow(X_train_m[idx], cmap='Blues')\n        axes[row][digit].set_title(str(digit), fontsize=10, fontweight='bold')\n        axes[row][digit].axis('off')\nplt.tight_layout()\nplt.show()\n\nfig, ax = plt.subplots(figsize=(11, 4), dpi=130)\nunique, counts = np.unique(y_train_m, return_counts=True)\nsns.barplot(x=unique, y=counts, palette='icefire', ax=ax, width=0.5)\nax.set_title(\"MNIST -- Class Distribution (Training Set)\", fontsize=12, fontweight='bold')\nax.set_xlabel(\"Digit Class\"); ax.set_ylabel(\"Number of Images\")\nfor bar, count in zip(ax.patches, counts):\n    ax.text(bar.get_x() + bar.get_width() / 2,\n            bar.get_height() + 30, f\"{count:,}\", ha='center', fontsize=9)\nax.set_ylim(0, 7500)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:42.312543Z","iopub.execute_input":"2026-04-26T23:00:42.313233Z","iopub.status.idle":"2026-04-26T23:00:43.636305Z","shell.execute_reply.started":"2026-04-26T23:00:42.313208Z","shell.execute_reply":"2026-04-26T23:00:43.635502Z"}},"outputs":[],"execution_count":null},{"id":"360816ee","cell_type":"markdown","source":"### B.2 Preprocessing\n\n| Step | Operation | Reason |\n|:---|:---|:---|\n| Normalization | Divide by 255 | Scale pixels to [0, 1] for stable training |\n| Reshape | Add channel dim | Conv2D expects (H, W, C); grayscale has no channel |\n| One-hot encode | to_categorical | categorical_crossentropy needs probability vectors |\n\nExample: label `3` becomes `[0, 0, 0, 1, 0, 0, 0, 0, 0, 0]`\n","metadata":{}},{"id":"471fdc07","cell_type":"code","source":"X_train_m = X_train_m.astype('float32') / 255.0\nX_test_m  = X_test_m.astype('float32')  / 255.0\n\nX_train_m = X_train_m[..., np.newaxis]\nX_test_m  = X_test_m[..., np.newaxis]\n\ny_train_m_oh = keras.utils.to_categorical(y_train_m, 10)\ny_test_m_oh  = keras.utils.to_categorical(y_test_m,  10)\n\nprint(\"Preprocessing complete.\")\nprint(f\"  X_train shape : {X_train_m.shape}   dtype: {X_train_m.dtype}\")\nprint(f\"  y_train[0] = digit {y_train_m[0]}  ->  {y_train_m_oh[0]}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:43.637332Z","iopub.execute_input":"2026-04-26T23:00:43.637588Z","iopub.status.idle":"2026-04-26T23:00:43.776667Z","shell.execute_reply.started":"2026-04-26T23:00:43.637567Z","shell.execute_reply":"2026-04-26T23:00:43.775834Z"}},"outputs":[],"execution_count":null},{"id":"5ac61cc1","cell_type":"markdown","source":"### B.3 CNN Architecture\n\n```\nInput: 28 x 28 x 1\n  +------------------------------------------------------------------+\n  |  Block 1 -- Conv2D(32) + BatchNorm + MaxPool(2) + Dropout(0.25)  |  14 x 14 x 32\n  |  Block 2 -- Conv2D(64) + BatchNorm + MaxPool(2) + Dropout(0.25)  |   7 x  7 x 64\n  |  Head    -- Flatten + Dense(128) + Dropout(0.5)                   |\n  |  Output  -- Dense(10) + Softmax                                   |  10 probabilities\n  +------------------------------------------------------------------+\n```\n\n**Why simpler than Cats vs Dogs?**\nMNIST images are 28x28 grayscale and highly standardized.\nTwo convolutional blocks are sufficient for state-of-the-art accuracy.\n","metadata":{}},{"id":"7d5dc26c","cell_type":"code","source":"model_mnist = Sequential([\n    Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(28, 28, 1)),\n    BatchNormalization(),\n    MaxPooling2D(2, 2),\n    Dropout(0.25),\n\n    Conv2D(64, (3, 3), activation='relu', padding='same'),\n    BatchNormalization(),\n    MaxPooling2D(2, 2),\n    Dropout(0.25),\n\n    Flatten(),\n    Dense(128, activation='relu'),\n    Dropout(0.5),\n    Dense(10, activation='softmax')\n], name=\"MNIST_CNN\")\n\nmodel_mnist.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])\nmodel_mnist.summary()\nprint(f\"\\nTotal parameters: {model_mnist.count_params():,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:43.777728Z","iopub.execute_input":"2026-04-26T23:00:43.778045Z","iopub.status.idle":"2026-04-26T23:00:43.857019Z","shell.execute_reply.started":"2026-04-26T23:00:43.778011Z","shell.execute_reply":"2026-04-26T23:00:43.856472Z"}},"outputs":[],"execution_count":null},{"id":"639c408d","cell_type":"markdown","source":"### B.4 Training & Evaluation","metadata":{}},{"id":"7ed6f797","cell_type":"code","source":"callbacks_mnist = [\n    EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True, verbose=1),\n    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, verbose=1)\n]\n\nprint(\"Training MNIST CNN...\")\nhistory_mnist = model_mnist.fit(\n    X_train_m, y_train_m_oh,\n    epochs=20, batch_size=128, validation_split=0.1,\n    callbacks=callbacks_mnist, verbose=1\n)\n\ntest_loss_m, test_acc_m = model_mnist.evaluate(X_test_m, y_test_m_oh, verbose=0)\nprint(f\"\\nTest Accuracy : {test_acc_m*100:.2f}%  |  Test Loss: {test_loss_m:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:00:43.857859Z","iopub.execute_input":"2026-04-26T23:00:43.858157Z","iopub.status.idle":"2026-04-26T23:01:30.415377Z","shell.execute_reply.started":"2026-04-26T23:00:43.858137Z","shell.execute_reply":"2026-04-26T23:01:30.414490Z"}},"outputs":[],"execution_count":null},{"id":"c8e1ffd5","cell_type":"code","source":"error_m = pd.DataFrame(history_mnist.history)\nfig, axes = plt.subplots(1, 2, figsize=(16, 5), dpi=150)\nfig.suptitle(\"MNIST CNN -- Training History\", fontsize=13, fontweight='bold')\n\naxes[0].plot(error_m['loss'],     lw=2.5, label='Train',      color='#f39c12')\naxes[0].plot(error_m['val_loss'], lw=2.5, label='Validation', color='#8e44ad', ls='--')\naxes[0].set_title(\"Loss\"); axes[0].set_xlabel(\"Epoch\"); axes[0].legend(); axes[0].grid(alpha=0.3)\n\naxes[1].plot(error_m['accuracy'],     lw=2.5, label='Train',      color='#f39c12')\naxes[1].plot(error_m['val_accuracy'], lw=2.5, label='Validation', color='#8e44ad', ls='--')\naxes[1].set_title(\"Accuracy\"); axes[1].set_xlabel(\"Epoch\"); axes[1].legend(); axes[1].grid(alpha=0.3)\nplt.tight_layout(); plt.show()\n\ny_pred_m = np.argmax(model_mnist.predict(X_test_m, verbose=0), axis=1)\ncm_m     = confusion_matrix(y_test_m, y_pred_m)\n\nfig, ax = plt.subplots(figsize=(9, 7), dpi=130)\nsns.heatmap(cm_m, annot=True, fmt='d', cmap='Blues', ax=ax,\n            linewidths=0.3, cbar=False, annot_kws={\"size\": 10})\nax.set_title(\"Confusion Matrix -- MNIST Test Set\", fontsize=12, fontweight='bold', pad=12)\nax.set_xlabel(\"Predicted Digit\"); ax.set_ylabel(\"True Digit\")\nplt.tight_layout(); plt.show()\n\nprint(\"\\nClassification Report:\")\nprint(classification_report(y_test_m, y_pred_m))\n\nfig, axes = plt.subplots(2, 10, figsize=(16, 4))\nfig.suptitle(\"MNIST Predictions  --  green = correct  |  red = incorrect\", fontsize=12, fontweight='bold')\nfor i in range(20):\n    ax = axes[i // 10][i % 10]\n    ax.imshow(X_test_m[i].squeeze(), cmap='Blues')\n    pred = y_pred_m[i]; true = y_test_m[i]\n    ax.set_title(f\"P:{pred}\", fontsize=9, color='#2ecc71' if pred==true else '#e74c3c', fontweight='bold')\n    ax.axis('off')\nplt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:01:30.416558Z","iopub.execute_input":"2026-04-26T23:01:30.416878Z","iopub.status.idle":"2026-04-26T23:01:33.438188Z","shell.execute_reply.started":"2026-04-26T23:01:30.416843Z","shell.execute_reply":"2026-04-26T23:01:33.437505Z"}},"outputs":[],"execution_count":null},{"id":"4da9fd45","cell_type":"markdown","source":"---\n**Part C &mdash; Fashion MNIST**\n---\n\n| Property | Value |\n|:---|:---|\n| Dataset | Fashion MNIST -- 70,000 grayscale images (60,000 train / 10,000 test) |\n| Source | Built into Keras -- no external download required |\n| Image size | 28 x 28 pixels |\n| Task | 10-class classification |\n\n| Label | Class | Label | Class |\n|:---:|:---|:---:|:---|\n| 0 | T-shirt / top | 5 | Sandal |\n| 1 | Trouser | 6 | Shirt |\n| 2 | Pullover | 7 | Sneaker |\n| 3 | Dress | 8 | Bag |\n| 4 | Coat | 9 | Ankle Boot |\n\n**Why harder than MNIST?**\nClothing items share visual features: Shirt vs T-shirt, Coat vs Pullover.\nThese fine-grained distinctions require a deeper model.\n\n![Fashion MNIST](https://i.imgur.com/xSg5QJS.jpeg)\n\n*Figure C.0: All 10 classes in the Fashion MNIST dataset.*\n","metadata":{}},{"id":"f28c9c3c","cell_type":"markdown","source":"### C.1 Load & Explore","metadata":{}},{"id":"708a23cb","cell_type":"code","source":"FASHION_CLASSES = [\n    'T-shirt', 'Trouser', 'Pullover', 'Dress', 'Coat',\n    'Sandal',  'Shirt',   'Sneaker',  'Bag',   'Ankle Boot'\n]\n\n(X_train_f, y_train_f), (X_test_f, y_test_f) = keras.datasets.fashion_mnist.load_data()\n\nX_train_f    = X_train_f.astype('float32') / 255.0\nX_test_f     = X_test_f.astype('float32')  / 255.0\nX_train_f    = X_train_f[..., np.newaxis]\nX_test_f     = X_test_f[..., np.newaxis]\ny_train_f_oh = keras.utils.to_categorical(y_train_f, 10)\ny_test_f_oh  = keras.utils.to_categorical(y_test_f,  10)\n\nprint(f\"Training : {X_train_f.shape}  |  Test: {X_test_f.shape}\")\n\npalette = ['#3498db','#e74c3c','#2ecc71','#9b59b6','#e67e22',\n           '#1abc9c','#f39c12','#c0392b','#16a085','#8e44ad']\n\nfig, axes = plt.subplots(2, 5, figsize=(15, 7))\nfig.suptitle(\"Fashion MNIST -- One Sample per Class\", fontsize=12, fontweight='bold')\nfor i, cls in enumerate(FASHION_CLASSES):\n    ax  = axes[i // 5][i % 5]\n    idx = np.where(y_train_f == i)[0][2]\n    ax.imshow(X_train_f[idx].squeeze(), cmap='plasma')\n    ax.set_title(cls, color=palette[i], fontsize=10, fontweight='bold')\n    ax.axis('off')\nplt.tight_layout(); plt.show()\n\nfig, ax = plt.subplots(figsize=(13, 4), dpi=130)\nunique_f, counts_f = np.unique(y_train_f, return_counts=True)\nsns.barplot(x=[FASHION_CLASSES[i] for i in unique_f], y=counts_f, palette=palette, ax=ax, width=0.6)\nax.set_title(\"Fashion MNIST -- Class Distribution\", fontsize=12, fontweight='bold')\nax.set_xlabel(\"Class\"); ax.set_ylabel(\"Images\")\nplt.xticks(rotation=30, ha='right')\nfor bar, count in zip(ax.patches, counts_f):\n    ax.text(bar.get_x() + bar.get_width() / 2,\n            bar.get_height() + 30, f\"{count:,}\", ha='center', fontsize=8)\nplt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:01:33.439154Z","iopub.execute_input":"2026-04-26T23:01:33.439469Z","iopub.status.idle":"2026-04-26T23:01:35.118922Z","shell.execute_reply.started":"2026-04-26T23:01:33.439434Z","shell.execute_reply":"2026-04-26T23:01:35.118081Z"}},"outputs":[],"execution_count":null},{"id":"7cb8df4e","cell_type":"markdown","source":"### C.2 CNN Architecture\n\n```\nInput: 28 x 28 x 1\n  +------------------------------------------------------------------------------+\n  |  Block 1 -- Conv2D(32)x2 + BN + MaxPool(2) + Dropout(0.3)   |  14 x 14 x 32  |\n  |  Block 2 -- Conv2D(64)x2 + BN + MaxPool(2) + Dropout(0.3)   |   7 x  7 x 64  |\n  |  Block 3 -- Conv2D(128)  + BN + MaxPool(2) + Dropout(0.3)   |   3 x  3 x 128 |\n  |  Head    -- Flatten + Dense(256) + BN + Dropout(0.5)        |                 |\n  |  Output  -- Dense(10) + Softmax                              |  10 probs       |\n  +------------------------------------------------------------------------------+\n```\n\n3 blocks (vs 2 for MNIST) to capture finer garment-level distinctions.\nDouble convolution per block increases receptive field without immediate pooling.\n","metadata":{}},{"id":"5fc79ff9","cell_type":"code","source":"model_fm = Sequential([\n    Conv2D(32, (3,3), activation='relu', padding='same', input_shape=(28,28,1)),\n    BatchNormalization(),\n    Conv2D(32, (3,3), activation='relu', padding='same'),\n    BatchNormalization(),\n    MaxPooling2D(2,2), Dropout(0.3),\n\n    Conv2D(64, (3,3), activation='relu', padding='same'),\n    BatchNormalization(),\n    Conv2D(64, (3,3), activation='relu', padding='same'),\n    BatchNormalization(),\n    MaxPooling2D(2,2), Dropout(0.3),\n\n    Conv2D(128, (3,3), activation='relu', padding='same'),\n    BatchNormalization(),\n    MaxPooling2D(2,2), Dropout(0.3),\n\n    Flatten(),\n    Dense(256, activation='relu'),\n    BatchNormalization(),\n    Dropout(0.5),\n    Dense(10, activation='softmax')\n], name=\"FashionMNIST_CNN\")\n\nmodel_fm.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])\nmodel_fm.summary()\nprint(f\"\\nTotal parameters: {model_fm.count_params():,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:01:35.119933Z","iopub.execute_input":"2026-04-26T23:01:35.120246Z","iopub.status.idle":"2026-04-26T23:01:35.250693Z","shell.execute_reply.started":"2026-04-26T23:01:35.120225Z","shell.execute_reply":"2026-04-26T23:01:35.250088Z"}},"outputs":[],"execution_count":null},{"id":"80df0be3","cell_type":"markdown","source":"### C.3 Training & Evaluation","metadata":{}},{"id":"39a0e730","cell_type":"code","source":"callbacks_fm = [\n    EarlyStopping(monitor='val_loss', patience=7, restore_best_weights=True, verbose=1),\n    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=4, verbose=1)\n]\n\nprint(\"Training Fashion MNIST CNN...\")\nhistory_fm = model_fm.fit(\n    X_train_f, y_train_f_oh,\n    epochs=25, batch_size=128, validation_split=0.1,\n    callbacks=callbacks_fm, verbose=1\n)\n\ntest_loss_f, test_acc_f = model_fm.evaluate(X_test_f, y_test_f_oh, verbose=0)\nprint(f\"\\nTest Accuracy : {test_acc_f*100:.2f}%  |  Test Loss: {test_loss_f:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:01:35.251548Z","iopub.execute_input":"2026-04-26T23:01:35.251847Z","iopub.status.idle":"2026-04-26T23:03:53.324625Z","shell.execute_reply.started":"2026-04-26T23:01:35.251827Z","shell.execute_reply":"2026-04-26T23:03:53.323822Z"}},"outputs":[],"execution_count":null},{"id":"2aa61d2e","cell_type":"code","source":"error_f = pd.DataFrame(history_fm.history)\nfig, axes = plt.subplots(1, 2, figsize=(16, 5), dpi=150)\nfig.suptitle(\"Fashion MNIST CNN -- Training History\", fontsize=13, fontweight='bold')\n\naxes[0].plot(error_f['loss'],     lw=2.5, label='Train',      color='#2ecc71')\naxes[0].plot(error_f['val_loss'], lw=2.5, label='Validation', color='#8e44ad', ls='--')\naxes[0].set_title(\"Loss\"); axes[0].set_xlabel(\"Epoch\"); axes[0].legend(); axes[0].grid(alpha=0.3)\n\naxes[1].plot(error_f['accuracy'],     lw=2.5, label='Train',      color='#2ecc71')\naxes[1].plot(error_f['val_accuracy'], lw=2.5, label='Validation', color='#8e44ad', ls='--')\naxes[1].set_title(\"Accuracy\"); axes[1].set_xlabel(\"Epoch\"); axes[1].legend(); axes[1].grid(alpha=0.3)\nplt.tight_layout(); plt.show()\n\ny_pred_f = np.argmax(model_fm.predict(X_test_f, verbose=0), axis=1)\ncm_f     = confusion_matrix(y_test_f, y_pred_f)\n\nfig, axes = plt.subplots(1, 2, figsize=(20, 7), dpi=130)\nfig.suptitle(\"Fashion MNIST -- Confusion Matrix\", fontsize=13, fontweight='bold')\n\nsns.heatmap(cm_f, annot=True, fmt='d', cmap='Blues',\n            xticklabels=FASHION_CLASSES, yticklabels=FASHION_CLASSES,\n            ax=axes[0], linewidths=0.3, cbar=False, annot_kws={\"size\": 9})\naxes[0].set_title(\"Counts\"); axes[0].set_xlabel(\"Predicted\"); axes[0].set_ylabel(\"True\")\nplt.setp(axes[0].get_xticklabels(), rotation=40, ha='right')\n\ncm_f_pct = cm_f.astype(float) / cm_f.sum(axis=1, keepdims=True) * 100\nsns.heatmap(cm_f_pct, annot=True, fmt='.1f', cmap='Blues',\n            xticklabels=FASHION_CLASSES, yticklabels=FASHION_CLASSES,\n            ax=axes[1], linewidths=0.3, cbar=False, annot_kws={\"size\": 9})\naxes[1].set_title(\"Row-Normalized (%)\"); axes[1].set_xlabel(\"Predicted\"); axes[1].set_ylabel(\"True\")\nplt.setp(axes[1].get_xticklabels(), rotation=40, ha='right')\n\nplt.tight_layout(); plt.show()\n\nprint(\"\\nClassification Report:\")\nprint(classification_report(y_test_f, y_pred_f, target_names=FASHION_CLASSES))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:03:53.325492Z","iopub.execute_input":"2026-04-26T23:03:53.325967Z","iopub.status.idle":"2026-04-26T23:03:56.226634Z","shell.execute_reply.started":"2026-04-26T23:03:53.325941Z","shell.execute_reply":"2026-04-26T23:03:56.225932Z"}},"outputs":[],"execution_count":null},{"id":"e882b147","cell_type":"markdown","source":"---\n<div align=\"center\">\n\n## D. Conclusion & Comparison\n\n</div>\n\n---\n","metadata":{}},{"id":"8b2951b5","cell_type":"code","source":"cvd_loss, cvd_acc = model.evaluate(test_generator, batch_size=BATCH_SIZE, verbose=0)\n\nresults = {\n    'Cats vs Dogs' : cvd_acc    * 100,\n    'MNIST'        : test_acc_m * 100,\n    'Fashion MNIST': test_acc_f * 100,\n}\n\nprint(\"=\" * 65)\nprint(f\"  {'Dataset':<18}  {'Task':<22}  {'Test Accuracy':>13}\")\nprint(\"=\" * 65)\nprint(f\"  {'Cats vs Dogs':<18}  {'Binary (2 classes)':<22}  {results['Cats vs Dogs']:>12.2f}%\")\nprint(f\"  {'MNIST':<18}  {'Multi  (10 classes)':<22}  {results['MNIST']:>12.2f}%\")\nprint(f\"  {'Fashion MNIST':<18}  {'Multi  (10 classes)':<22}  {results['Fashion MNIST']:>12.2f}%\")\nprint(\"=\" * 65)\n\nfig, ax = plt.subplots(figsize=(11, 6), dpi=150)\nbars = ax.bar(results.keys(), results.values(),\n              color=['#3498db', '#f39c12', '#2ecc71'],\n              edgecolor='black', linewidth=0.8, width=0.45)\nax.set_ylabel('Test Accuracy (%)', fontsize=12)\nax.set_ylim(0, 112)\nax.set_title('Test Accuracy Comparison -- All Three Datasets', fontsize=13, fontweight='bold')\nax.axhline(y=90, color='gray', ls='--', alpha=0.6, lw=1.5, label='90% reference')\nax.legend(fontsize=10); ax.grid(axis='y', alpha=0.3)\n\nfor bar, acc in zip(bars, results.values()):\n    ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.8,\n            f'{acc:.2f}%', ha='center', va='bottom', fontsize=13, fontweight='bold')\n\nplt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-26T23:03:56.227436Z","iopub.execute_input":"2026-04-26T23:03:56.227823Z","iopub.status.idle":"2026-04-26T23:03:59.728130Z","shell.execute_reply.started":"2026-04-26T23:03:56.227801Z","shell.execute_reply":"2026-04-26T23:03:59.727480Z"}},"outputs":[],"execution_count":null},{"id":"3dbb4d14","cell_type":"markdown","source":"### Key Findings\n\n**1. Architecture depth must match task complexity.**\nCats vs Dogs required 4 convolutional blocks (32 to 64 to 128 to 256 filters).\nFashion MNIST needed 3 blocks. MNIST required only 2.\n\n**2. Data augmentation is essential for real-world photographic data.**\nFor MNIST and Fashion MNIST, standardized images require no augmentation.\nFor Cats vs Dogs, augmentation (flip, rotation, zoom) was critical to prevent overfitting.\n\n**3. `flow_from_dataframe` is the correct approach on Kaggle.**\nIt accepts an explicit filename-to-label mapping and works on read-only filesystems,\nunlike `flow_from_directory` which requires images pre-sorted into class folders.\n\n**4. BatchNormalization and Dropout are complementary.**\nBatchNorm stabilizes activations; Dropout prevents neuron co-adaptation.\nTogether they enable deeper networks without explicit weight decay.\n\n**5. EarlyStopping with `restore_best_weights=True` is essential.**\nIt halts training when validation loss plateaus and saves the best model automatically.\n\n**6. Confusion matrices expose class-level failure modes.**\nFor Fashion MNIST: Shirt vs T-shirt and Coat vs Pullover are the hardest pairs.\nFor MNIST: digit pairs 4/9 and 3/8 produce the most errors.\n\n---\n\n### Acknowledgements\n\n**Dr. Sherif Al-Itreby** &mdash; for his expert instruction in Deep Learning theory and practice,\nand for designing the project framework that motivated this investigation.\n\n**Eng. Omar Amir** &mdash; for his technical mentorship and invaluable assistance\nduring the implementation phase.\n","metadata":{}}]}