{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Cats vs Dogs — CNN Classifier (From Scratch → Transfer Learning → Fine-Tuning)This notebook is a binary image classification project (cat vs dog), built in three stages:1. **CNN from scratch** — a custom Conv2D-based model, trained with no pretrained weights.2. **Data Augmentation** — same custom model, but training images are randomly flipped/rotated/zoomed to reduce overfitting.3. **Transfer Learning (VGG16)** — using a pretrained VGG16 (trained on ImageNet) to extract features, then training a small classifier on top of those features.4. **Fine-Tuning** — unfreezing the last few layers of VGG16 and retraining the whole model with a very small learning rate.Dataset: Kaggle's **Dogs vs Cats** competition (25,000 images). We only use a small subset of 2,500 images (1000 train, 500 validation, 1000 test per class) to keep training fast and to clearly show the effect of overfitting.\n","metadata":{}},{"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# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:23.380217Z","iopub.execute_input":"2026-07-25T15:26:23.381108Z","iopub.status.idle":"2026-07-25T15:26:24.519260Z","shell.execute_reply.started":"2026-07-25T15:26:23.381066Z","shell.execute_reply":"2026-07-25T15:26:24.518701Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Data SetupDownload the dataset zip from Kaggle and extract it into `/kaggle/working/`. Then build a small subset from it — separate train/validation/test folders, each containing cat and dog images.","metadata":{}},{"cell_type":"code","source":"import zipfile\n\nwith zipfile.ZipFile(\"/kaggle/input/competitions/dogs-vs-cats/train.zip\", \"r\") as zip_ref:\n    zip_ref.extractall(\"/kaggle/working/train\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:24.520793Z","iopub.execute_input":"2026-07-25T15:26:24.521376Z","iopub.status.idle":"2026-07-25T15:26:31.072723Z","shell.execute_reply.started":"2026-07-25T15:26:24.521352Z","shell.execute_reply":"2026-07-25T15:26:31.072095Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pathlib\noriginal_dir = pathlib.Path(\"/kaggle/working/train/train\")  # zip ke andar ek aur train/ folder ho sakta hai, check kar lena\nnew_base_dir = pathlib.Path(\"/kaggle/working/cats_vs_dogs_small\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:31.073701Z","iopub.execute_input":"2026-07-25T15:26:31.074194Z","iopub.status.idle":"2026-07-25T15:26:31.077862Z","shell.execute_reply.started":"2026-07-25T15:26:31.074170Z","shell.execute_reply":"2026-07-25T15:26:31.077075Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, shutil\ndef make_subset(subset_name, start_index, end_index):\n  for category in (\"cat\", \"dog\"):\n    dir = new_base_dir / subset_name / category\n    os.makedirs(dir)\n    fnames = [f\"{category}.{i}.jpg\"\n    for i in range(start_index, end_index)]\n    for fname in fnames:\n      shutil.copyfile(src=original_dir / fname,dst=dir / fname)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:31.078808Z","iopub.execute_input":"2026-07-25T15:26:31.079314Z","iopub.status.idle":"2026-07-25T15:26:31.090386Z","shell.execute_reply.started":"2026-07-25T15:26:31.079292Z","shell.execute_reply":"2026-07-25T15:26:31.089716Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"make_subset(\"train\", start_index=0, end_index=1000)\nmake_subset(\"validation\", start_index=1000, end_index=1500)\nmake_subset(\"test\", start_index=1500, end_index=2500)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:31.092112Z","iopub.execute_input":"2026-07-25T15:26:31.092389Z","iopub.status.idle":"2026-07-25T15:26:31.695742Z","shell.execute_reply.started":"2026-07-25T15:26:31.092369Z","shell.execute_reply":"2026-07-25T15:26:31.694945Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow import keras\nfrom tensorflow.keras import layers","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:31.696845Z","iopub.execute_input":"2026-07-25T15:26:31.697155Z","iopub.status.idle":"2026-07-25T15:26:45.439442Z","shell.execute_reply.started":"2026-07-25T15:26:31.697120Z","shell.execute_reply":"2026-07-25T15:26:45.438867Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Baseline Model — CNN From ScratchA simple architecture: 4 Conv2D + MaxPooling blocks, followed by a Flatten layer and a Dense output layer (sigmoid activation, since this is binary classification — cat=0, dog=1).Since the dataset is small, expect overfitting — training accuracy will be high, but validation accuracy will lag behind.","metadata":{}},{"cell_type":"code","source":"inputs = keras.Input(shape=(180, 180, 3))\nx = layers.Rescaling(1./255)(inputs)\nx = layers.Conv2D(filters=32, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=64, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=128, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=256, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=256, kernel_size=3, activation=\"relu\")(x)\nx = layers.Flatten()(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\nmodel = keras.Model(inputs=inputs, outputs=outputs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:45.440634Z","iopub.execute_input":"2026-07-25T15:26:45.441163Z","iopub.status.idle":"2026-07-25T15:26:47.724478Z","shell.execute_reply.started":"2026-07-25T15:26:45.441138Z","shell.execute_reply":"2026-07-25T15:26:47.723916Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Check the model summaryShows all layers, their output shapes, and the total number of trainable parameters.","metadata":{}},{"cell_type":"code","source":"model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:47.725312Z","iopub.execute_input":"2026-07-25T15:26:47.725639Z","iopub.status.idle":"2026-07-25T15:26:47.748242Z","shell.execute_reply.started":"2026-07-25T15:26:47.725612Z","shell.execute_reply":"2026-07-25T15:26:47.747637Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Compile the modelSince this is binary classification, we use `binary_crossentropy` as the loss function and `rmsprop` as the optimizer.","metadata":{}},{"cell_type":"code","source":"model.compile(loss=\"binary_crossentropy\",\n optimizer=\"rmsprop\",\n metrics=[\"accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:47.749136Z","iopub.execute_input":"2026-07-25T15:26:47.749432Z","iopub.status.idle":"2026-07-25T15:26:47.922312Z","shell.execute_reply.started":"2026-07-25T15:26:47.749410Z","shell.execute_reply":"2026-07-25T15:26:47.921654Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load images as a `tf.data.Dataset``image_dataset_from_directory` automatically assigns labels based on folder names (cat/dog), resizes images to 180x180, and groups them into batches of 32.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.utils import image_dataset_from_directory\ntrain_dataset = image_dataset_from_directory(\n new_base_dir / \"train\",\n image_size=(180, 180),\n batch_size=32)\nvalidation_dataset = image_dataset_from_directory(\n new_base_dir / \"validation\",\n image_size=(180, 180),\n batch_size=32)\ntest_dataset = image_dataset_from_directory(\n new_base_dir / \"test\",\n image_size=(180, 180),\n batch_size=32)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:47.923260Z","iopub.execute_input":"2026-07-25T15:26:47.923657Z","iopub.status.idle":"2026-07-25T15:26:49.251699Z","shell.execute_reply.started":"2026-07-25T15:26:47.923603Z","shell.execute_reply":"2026-07-25T15:26:49.250882Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Sanity check — how `tf.data.Dataset` works**Note:** The next 4 cells are just a demo — they show how a `tf.data.Dataset` object gets batched, and how `.map()` can reshape data. They are not part of the actual training pipeline, so feel free to skip them.","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport tensorflow as tf\nrandom_numbers = np.random.normal(size=(1000, 16))\ndataset = tf.data.Dataset.from_tensor_slices(random_numbers)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.252986Z","iopub.execute_input":"2026-07-25T15:26:49.253345Z","iopub.status.idle":"2026-07-25T15:26:49.260037Z","shell.execute_reply.started":"2026-07-25T15:26:49.253322Z","shell.execute_reply":"2026-07-25T15:26:49.259194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for i, element in enumerate(dataset):\n  print(element.shape)\n  if i >= 2:\n    break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.260965Z","iopub.execute_input":"2026-07-25T15:26:49.261161Z","iopub.status.idle":"2026-07-25T15:26:49.278165Z","shell.execute_reply.started":"2026-07-25T15:26:49.261141Z","shell.execute_reply":"2026-07-25T15:26:49.277548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"batched_dataset = dataset.batch(32)\nfor i, element in enumerate(batched_dataset):\n  print(element.shape)\n  if i >= 2:\n      break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.279139Z","iopub.execute_input":"2026-07-25T15:26:49.279357Z","iopub.status.idle":"2026-07-25T15:26:49.290816Z","shell.execute_reply.started":"2026-07-25T15:26:49.279335Z","shell.execute_reply":"2026-07-25T15:26:49.290020Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"reshaped_dataset = dataset.map(lambda x: tf.reshape(x, (4, 4)))\nfor i, element in enumerate(reshaped_dataset):\n  print(element.shape)\n  if i >= 2:\n    break\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.293778Z","iopub.execute_input":"2026-07-25T15:26:49.294189Z","iopub.status.idle":"2026-07-25T15:26:49.329004Z","shell.execute_reply.started":"2026-07-25T15:26:49.294167Z","shell.execute_reply":"2026-07-25T15:26:49.328420Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Check the actual dataset shapeConfirms what shape a single batch from `train_dataset` has — images should be `(32, 180, 180, 3)` and labels `(32,)`.","metadata":{}},{"cell_type":"code","source":"for data_batch, labels_batch in train_dataset:\n  print(\"data batch shape:\", data_batch.shape)\n  print(\"labels batch shape:\", labels_batch.shape)\n  break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.329886Z","iopub.execute_input":"2026-07-25T15:26:49.330177Z","iopub.status.idle":"2026-07-25T15:26:49.392257Z","shell.execute_reply.started":"2026-07-25T15:26:49.330148Z","shell.execute_reply":"2026-07-25T15:26:49.391335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Train the Baseline ModelA `ModelCheckpoint` callback is used, which automatically saves the best model (lowest validation loss) to a `.keras` file — so we don't have to wait for the entire training run to finish to get the best version.⚠️ This will take **30 epochs** — a few minutes on GPU, longer on CPU.","metadata":{}},{"cell_type":"code","source":"callbacks = [\n    keras.callbacks.ModelCheckpoint(\n        filepath=\"convnet_from_scratch.keras\",\n        save_best_only=True,\n        monitor=\"val_loss\")\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.393274Z","iopub.execute_input":"2026-07-25T15:26:49.393894Z","iopub.status.idle":"2026-07-25T15:26:49.397841Z","shell.execute_reply.started":"2026-07-25T15:26:49.393857Z","shell.execute_reply":"2026-07-25T15:26:49.397053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = model.fit(\n    train_dataset,\n    epochs=30,\n    validation_data=validation_dataset,    callbacks=callbacks)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:26:49.398767Z","iopub.execute_input":"2026-07-25T15:26:49.399031Z","iopub.status.idle":"2026-07-25T15:27:58.706317Z","shell.execute_reply.started":"2026-07-25T15:26:49.398999Z","shell.execute_reply":"2026-07-25T15:27:58.705702Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Plot the training curvesTraining vs validation accuracy/loss graphs — if the two curves drift apart (train going up, val flat or dropping), that means overfitting.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\naccuracy = history.history[\"accuracy\"]\nval_accuracy = history.history[\"val_accuracy\"]\nloss = history.history[\"loss\"]\nval_loss = history.history[\"val_loss\"]\nepochs = range(1, len(accuracy) + 1)\nplt.plot(epochs, accuracy, \"bo\", label=\"Training accuracy\")\nplt.plot(epochs, val_accuracy, \"b\", label=\"Validation accuracy\")\nplt.title(\"Training and validation accuracy\")\nplt.legend()\nplt.figure()\nplt.plot(epochs, loss, \"bo\", label=\"Training loss\")\nplt.plot(epochs, val_loss, \"b\", label=\"Validation loss\")\nplt.title(\"Training and validation loss\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:27:58.707263Z","iopub.execute_input":"2026-07-25T15:27:58.707598Z","iopub.status.idle":"2026-07-25T15:27:59.017402Z","shell.execute_reply.started":"2026-07-25T15:27:58.707546Z","shell.execute_reply":"2026-07-25T15:27:59.016415Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Evaluate on the test setLoad the best saved model (from the checkpoint) and check its final accuracy on the test set.","metadata":{}},{"cell_type":"code","source":"test_model = keras.models.load_model(\"convnet_from_scratch.keras\")\ntest_loss, test_acc = test_model.evaluate(test_dataset)\nprint(f\"Test accuracy: {test_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:27:59.018535Z","iopub.execute_input":"2026-07-25T15:27:59.018913Z","iopub.status.idle":"2026-07-25T15:28:01.401561Z","shell.execute_reply.started":"2026-07-25T15:27:59.018889Z","shell.execute_reply":"2026-07-25T15:28:01.400964Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Data AugmentationBecause the dataset is small (2000 images), the risk of overfitting is high. Data augmentation artificially increases variation by randomly flipping, rotating, and zooming training images — so the model doesn't see the exact same image over and over.","metadata":{}},{"cell_type":"code","source":"data_augmentation = keras.Sequential(\n [\n layers.RandomFlip(\"horizontal\"),\n layers.RandomRotation(0.1),\n layers.RandomZoom(0.2),\n ]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:01.402502Z","iopub.execute_input":"2026-07-25T15:28:01.402813Z","iopub.status.idle":"2026-07-25T15:28:01.415644Z","shell.execute_reply.started":"2026-07-25T15:28:01.402777Z","shell.execute_reply":"2026-07-25T15:28:01.414976Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Preview augmented imagesThe same image is augmented 9 times, just to show what the randomness looks like (horizontal flip, slight rotation, slight zoom).","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize=(10, 10))\nfor images, _ in train_dataset.take(1):\n  for i in range(9):\n    augmented_images = data_augmentation(images)\n    ax = plt.subplot(3, 3, i + 1)\n    plt.imshow(augmented_images[0].numpy().astype(\"uint8\"))\n    plt.axis(\"off\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:01.416720Z","iopub.execute_input":"2026-07-25T15:28:01.417595Z","iopub.status.idle":"2026-07-25T15:28:03.065128Z","shell.execute_reply.started":"2026-07-25T15:28:01.417543Z","shell.execute_reply":"2026-07-25T15:28:03.064254Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### New model — with an augmentation layerSame CNN architecture as before, but now a `data_augmentation` layer is added right after the input. This means the model sees slightly different (augmented) images every epoch, which reduces overfitting.","metadata":{}},{"cell_type":"code","source":"inputs = keras.Input(shape=(180, 180, 3))\nx = data_augmentation(inputs)\nx = layers.Rescaling(1./255)(x)\nx = layers.Conv2D(filters=32, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=64, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=128, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=256, kernel_size=3, activation=\"relu\")(x)\nx = layers.MaxPooling2D(pool_size=2)(x)\nx = layers.Conv2D(filters=256, kernel_size=3, activation=\"relu\")(x)\nx = layers.Flatten()(x)\nx = layers.Dropout(0.5)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\nmodel = keras.Model(inputs=inputs, outputs=outputs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:03.066133Z","iopub.execute_input":"2026-07-25T15:28:03.066333Z","iopub.status.idle":"2026-07-25T15:28:03.129340Z","shell.execute_reply.started":"2026-07-25T15:28:03.066313Z","shell.execute_reply":"2026-07-25T15:28:03.128649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(loss=\"binary_crossentropy\",\n              optimizer=\"rmsprop\",\n              metrics=[\"accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:03.130349Z","iopub.execute_input":"2026-07-25T15:28:03.130702Z","iopub.status.idle":"2026-07-25T15:28:03.138722Z","shell.execute_reply.started":"2026-07-25T15:28:03.130679Z","shell.execute_reply":"2026-07-25T15:28:03.137919Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n keras.callbacks.ModelCheckpoint(\n filepath=\"convnet_from_scratch_with_augmentation.keras\",\n save_best_only=True,\n monitor=\"val_loss\")\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:03.139695Z","iopub.execute_input":"2026-07-25T15:28:03.139970Z","iopub.status.idle":"2026-07-25T15:28:03.145708Z","shell.execute_reply.started":"2026-07-25T15:28:03.139916Z","shell.execute_reply":"2026-07-25T15:28:03.144951Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history = model.fit(\n    train_dataset,\n    epochs=30,\n    validation_data=validation_dataset,\n    callbacks=callbacks\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:28:03.146699Z","iopub.execute_input":"2026-07-25T15:28:03.147000Z","iopub.status.idle":"2026-07-25T15:29:09.440043Z","shell.execute_reply.started":"2026-07-25T15:28:03.146978Z","shell.execute_reply":"2026-07-25T15:29:09.439399Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train the augmented modelSame 30 epochs, but this time expect train and validation accuracy to stay closer together compared to the earlier (non-augmented) model — meaning less overfitting.","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\naccuracy = history.history[\"accuracy\"]\nval_accuracy = history.history[\"val_accuracy\"]\nloss = history.history[\"loss\"]\nval_loss = history.history[\"val_loss\"]\nepochs = range(1, len(accuracy) + 1)\nplt.plot(epochs, accuracy, \"bo\", label=\"Training accuracy\")\nplt.plot(epochs, val_accuracy, \"b\", label=\"Validation accuracy\")\nplt.title(\"Training and validation accuracy\")\nplt.legend()\nplt.figure()\nplt.plot(epochs, loss, \"bo\", label=\"Training loss\")\nplt.plot(epochs, val_loss, \"b\", label=\"Validation loss\")\nplt.title(\"Training and validation loss\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:09.441091Z","iopub.execute_input":"2026-07-25T15:29:09.441388Z","iopub.status.idle":"2026-07-25T15:29:09.691847Z","shell.execute_reply.started":"2026-07-25T15:29:09.441367Z","shell.execute_reply":"2026-07-25T15:29:09.691166Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Test accuracy of the augmented model","metadata":{}},{"cell_type":"code","source":"test_model = keras.models.load_model(\n            \"convnet_from_scratch_with_augmentation.keras\")\ntest_loss, test_acc = test_model.evaluate(test_dataset)\nprint(f\"Test accuracy: {test_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:09.692870Z","iopub.execute_input":"2026-07-25T15:29:09.693203Z","iopub.status.idle":"2026-07-25T15:29:11.412753Z","shell.execute_reply.started":"2026-07-25T15:29:09.693179Z","shell.execute_reply":"2026-07-25T15:29:11.411931Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Transfer Learning — VGG16 Feature ExtractionInstead of our own CNN, we now use a pretrained model: **VGG16**, already trained on ImageNet (1.4 million images, 1000 classes). `include_top=False` means we're only taking its convolutional base (the feature extractor), not its original classifier (the 1000-class output).Idea: VGG16 has already learned general image features (edges, textures, shapes). We can reuse those to train a small cat/dog classifier faster and more accurately.","metadata":{}},{"cell_type":"code","source":"conv_base = keras.applications.vgg16.VGG16(\n            weights=\"imagenet\",\n            include_top=False,\n            input_shape=(180, 180, 3)\n            )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:11.413707Z","iopub.execute_input":"2026-07-25T15:29:11.414077Z","iopub.status.idle":"2026-07-25T15:29:12.755796Z","shell.execute_reply.started":"2026-07-25T15:29:11.414050Z","shell.execute_reply":"2026-07-25T15:29:12.754916Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Look at the VGG16 architecture","metadata":{}},{"cell_type":"code","source":"conv_base.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:12.756787Z","iopub.execute_input":"2026-07-25T15:29:12.757257Z","iopub.status.idle":"2026-07-25T15:29:12.782846Z","shell.execute_reply.started":"2026-07-25T15:29:12.757232Z","shell.execute_reply":"2026-07-25T15:29:12.782057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Extract features (just once)Each image is passed through VGG16 to get its output feature map — we compute this once and store it (for train/val/test), so we don't have to pass images through VGG16 repeatedly. This makes training much faster, since VGG16 itself isn't being trained — only feature extraction is happening.**Bug fix note:** Previously, the `return` statement in this function was inside the `for` loop, so only the first batch (32 images) was ever processed and returned. Now `return` is outside the loop, so the entire dataset (all batches) is processed and concatenated.","metadata":{}},{"cell_type":"code","source":"import numpy as np\ndef get_features_and_labels(dataset):\n  all_features = []\n  all_labels = []\n  for images, labels in dataset:\n    preprocessed_images = keras.applications.vgg16.preprocess_input(images)\n    features = conv_base.predict(preprocessed_images)\n    all_features.append(features)\n    all_labels.append(labels)\n  return np.concatenate(all_features), np.concatenate(all_labels)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:12.783851Z","iopub.execute_input":"2026-07-25T15:29:12.784125Z","iopub.status.idle":"2026-07-25T15:29:12.788646Z","shell.execute_reply.started":"2026-07-25T15:29:12.784095Z","shell.execute_reply":"2026-07-25T15:29:12.787929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_features, train_labels = get_features_and_labels(train_dataset)\nval_features, val_labels = get_features_and_labels(validation_dataset)\ntest_features, test_labels = get_features_and_labels(test_dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:12.789630Z","iopub.execute_input":"2026-07-25T15:29:12.790099Z","iopub.status.idle":"2026-07-25T15:29:43.876561Z","shell.execute_reply.started":"2026-07-25T15:29:12.790065Z","shell.execute_reply":"2026-07-25T15:29:43.875639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Check the shape of the extracted featuresShould be something like `(1000, 5, 5, 512)` — 1000 training images, each with a VGG16 output feature map of size 5x5x512.","metadata":{}},{"cell_type":"code","source":"train_features.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:43.877645Z","iopub.execute_input":"2026-07-25T15:29:43.878249Z","iopub.status.idle":"2026-07-25T15:29:43.883630Z","shell.execute_reply.started":"2026-07-25T15:29:43.878222Z","shell.execute_reply":"2026-07-25T15:29:43.883028Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Build a small classifier — on top of the extracted featuresInstead of VGG16, we now train a simple Dense classifier that takes these extracted features as input. This trains very fast since the convolution part is already done.","metadata":{}},{"cell_type":"code","source":"inputs = keras.Input(shape=(5, 5, 512))\nx = layers.Flatten()(inputs)\nx = layers.Dense(256)(x)\nx = layers.Dropout(0.5)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\nmodel = keras.Model(inputs, outputs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:43.884379Z","iopub.execute_input":"2026-07-25T15:29:43.885015Z","iopub.status.idle":"2026-07-25T15:29:43.918938Z","shell.execute_reply.started":"2026-07-25T15:29:43.884992Z","shell.execute_reply":"2026-07-25T15:29:43.918389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(loss=\"binary_crossentropy\",\n              optimizer=\"rmsprop\",\n              metrics=[\"accuracy\"]\n              )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:43.919674Z","iopub.execute_input":"2026-07-25T15:29:43.919951Z","iopub.status.idle":"2026-07-25T15:29:43.927302Z","shell.execute_reply.started":"2026-07-25T15:29:43.919930Z","shell.execute_reply":"2026-07-25T15:29:43.926708Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n              keras.callbacks.ModelCheckpoint(\n              filepath=\"feature_extraction.keras\",\n              save_best_only=True,\n              monitor=\"val_loss\")\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:43.928168Z","iopub.execute_input":"2026-07-25T15:29:43.928711Z","iopub.status.idle":"2026-07-25T15:29:43.932878Z","shell.execute_reply.started":"2026-07-25T15:29:43.928689Z","shell.execute_reply":"2026-07-25T15:29:43.932258Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train the feature-extraction modelOnly 20 epochs needed since the convolutional features are already extracted — only the Dense layers are training, which is fast.","metadata":{}},{"cell_type":"code","source":"history = model.fit(\n        train_features, train_labels,\n        epochs=20,\n        validation_data=(val_features, val_labels),\n        callbacks=callbacks\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:43.933817Z","iopub.execute_input":"2026-07-25T15:29:43.934128Z","iopub.status.idle":"2026-07-25T15:29:53.446099Z","shell.execute_reply.started":"2026-07-25T15:29:43.934081Z","shell.execute_reply":"2026-07-25T15:29:53.445196Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training curves — feature extraction modelExpect these curves to look better than both previous models (higher validation accuracy, less overfitting) since VGG16's pretrained features are strong.","metadata":{}},{"cell_type":"code","source":"accuracy = history.history[\"accuracy\"]\nval_accuracy = history.history[\"val_accuracy\"]\nloss = history.history[\"loss\"]\nval_loss = history.history[\"val_loss\"]\nepochs = range(1, len(accuracy) + 1)\nplt.plot(epochs, accuracy, \"bo\", label=\"Training accuracy\")\nplt.plot(epochs, val_accuracy, \"b\", label=\"Validation accuracy\")\nplt.title(\"Training and validation accuracy\")\nplt.legend()\nplt.figure()\nplt.plot(epochs, loss, \"bo\", label=\"Training loss\")\nplt.plot(epochs, val_loss, \"b\", label=\"Validation loss\")\nplt.title(\"Training and validation loss\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:53.447242Z","iopub.execute_input":"2026-07-25T15:29:53.447934Z","iopub.status.idle":"2026-07-25T15:29:53.726346Z","shell.execute_reply.started":"2026-07-25T15:29:53.447909Z","shell.execute_reply":"2026-07-25T15:29:53.725778Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. End-to-End Model with VGG16 (Frozen) + Data AugmentationA slightly different approach: this time VGG16 is embedded inside the full model (instead of extracting features separately), combined with data augmentation. `conv_base.trainable = False` freezes VGG16's weights — meaning VGG16 itself won't update during training, only our new Dense classifier will.","metadata":{}},{"cell_type":"code","source":"conv_base = keras.applications.vgg16.VGG16(\n weights=\"imagenet\",\n include_top=False)\nconv_base.trainable = False\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:53.727154Z","iopub.execute_input":"2026-07-25T15:29:53.727489Z","iopub.status.idle":"2026-07-25T15:29:53.989951Z","shell.execute_reply.started":"2026-07-25T15:29:53.727467Z","shell.execute_reply":"2026-07-25T15:29:53.989317Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Confirm whether VGG16 is frozenCheck the count of trainable weights to confirm the freeze/unfreeze worked correctly.","metadata":{}},{"cell_type":"code","source":"conv_base.trainable = True\nprint(\"This is the number of trainable weights \"\n \"before freezing the conv base:\", len(conv_base.trainable_weights))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:53.990905Z","iopub.execute_input":"2026-07-25T15:29:53.991208Z","iopub.status.idle":"2026-07-25T15:29:53.996338Z","shell.execute_reply.started":"2026-07-25T15:29:53.991183Z","shell.execute_reply":"2026-07-25T15:29:53.995552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"conv_base.trainable = False\nprint(\"This is the number of trainable weights \"\n \"after freezing the conv base:\", len(conv_base.trainable_weights))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:53.997305Z","iopub.execute_input":"2026-07-25T15:29:53.997643Z","iopub.status.idle":"2026-07-25T15:29:54.010541Z","shell.execute_reply.started":"2026-07-25T15:29:53.997615Z","shell.execute_reply":"2026-07-25T15:29:54.009931Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### New model — augmentation + frozen VGG16 + classifierThis time VGG16 is inside the full model pipeline (in the earlier approach features were extracted separately). This lets augmentation work properly, since images pass through VGG16 again (in augmented form) every epoch.","metadata":{}},{"cell_type":"code","source":"data_augmentation = keras.Sequential(\n [\n layers.RandomFlip(\"horizontal\"),\n layers.RandomRotation(0.1),\n layers.RandomZoom(0.2),\n ]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:54.015140Z","iopub.execute_input":"2026-07-25T15:29:54.015647Z","iopub.status.idle":"2026-07-25T15:29:54.034398Z","shell.execute_reply.started":"2026-07-25T15:29:54.015624Z","shell.execute_reply":"2026-07-25T15:29:54.033636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inputs = keras.Input(shape=(180, 180, 3))\nx = data_augmentation(inputs)\nx = keras.applications.vgg16.preprocess_input(x)\nx = conv_base(x)\nx = layers.Flatten()(x)\nx = layers.Dense(256)(x)\nx = layers.Dropout(0.5)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\nmodel = keras.Model(inputs, outputs)\nmodel.compile(loss=\"binary_crossentropy\",\n optimizer=\"rmsprop\",\n metrics=[\"accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:54.035384Z","iopub.execute_input":"2026-07-25T15:29:54.035886Z","iopub.status.idle":"2026-07-25T15:29:54.080093Z","shell.execute_reply.started":"2026-07-25T15:29:54.035854Z","shell.execute_reply":"2026-07-25T15:29:54.079403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n keras.callbacks.ModelCheckpoint(\n filepath=\"feature_extraction_with_data_augmentation.keras\",\n save_best_only=True,\n monitor=\"val_loss\")\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:54.081008Z","iopub.execute_input":"2026-07-25T15:29:54.081311Z","iopub.status.idle":"2026-07-25T15:29:54.085383Z","shell.execute_reply.started":"2026-07-25T15:29:54.081277Z","shell.execute_reply":"2026-07-25T15:29:54.084639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train (50 epochs)This will take longer than before since every batch now has to pass through VGG16 (instead of using pre-extracted features). But we get the benefit of data augmentation.","metadata":{}},{"cell_type":"code","source":"history = model.fit(\n train_dataset,\n epochs=50,\n validation_data=validation_dataset,\n callbacks=callbacks)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:29:54.086439Z","iopub.execute_input":"2026-07-25T15:29:54.086767Z","iopub.status.idle":"2026-07-25T15:33:54.026120Z","shell.execute_reply.started":"2026-07-25T15:29:54.086733Z","shell.execute_reply":"2026-07-25T15:33:54.025461Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Test accuracy — augmented + frozen VGG16 model","metadata":{}},{"cell_type":"code","source":" test_model = keras.models.load_model(\n \"feature_extraction_with_data_augmentation.keras\")\ntest_loss, test_acc = test_model.evaluate(test_dataset)\nprint(f\"Test accuracy: {test_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:54.027180Z","iopub.execute_input":"2026-07-25T15:33:54.027515Z","iopub.status.idle":"2026-07-25T15:33:57.836704Z","shell.execute_reply.started":"2026-07-25T15:33:54.027493Z","shell.execute_reply":"2026-07-25T15:33:57.836065Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Fine-TuningNow we **unfreeze the last few layers** of VGG16 (everything else stays frozen), so those layers can adjust slightly to our specific dataset (cats/dogs). We only unfreeze the last layers because early layers learn generic features (edges, colors) that are always useful, while the last layers are more task-specific.⚠️ Fine-tuning requires a very **small learning rate** (`1e-5`) — otherwise large updates could damage the pretrained weights and the model could lose what it already learned.","metadata":{}},{"cell_type":"code","source":"conv_base.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:57.837521Z","iopub.execute_input":"2026-07-25T15:33:57.837832Z","iopub.status.idle":"2026-07-25T15:33:57.863849Z","shell.execute_reply.started":"2026-07-25T15:33:57.837810Z","shell.execute_reply":"2026-07-25T15:33:57.863263Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Unfreeze the last 4 layers, keep the rest frozen","metadata":{}},{"cell_type":"code","source":"conv_base.trainable = True\nfor layer in conv_base.layers[:-4]:\n layer.trainable = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:57.864799Z","iopub.execute_input":"2026-07-25T15:33:57.865148Z","iopub.status.idle":"2026-07-25T15:33:57.869358Z","shell.execute_reply.started":"2026-07-25T15:33:57.865126Z","shell.execute_reply":"2026-07-25T15:33:57.868635Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Compile — with a small learning rate","metadata":{}},{"cell_type":"code","source":"model.compile(loss=\"binary_crossentropy\",\n optimizer=keras.optimizers.RMSprop(learning_rate=1e-5),\n metrics=[\"accuracy\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:57.870173Z","iopub.execute_input":"2026-07-25T15:33:57.870543Z","iopub.status.idle":"2026-07-25T15:33:57.893769Z","shell.execute_reply.started":"2026-07-25T15:33:57.870522Z","shell.execute_reply":"2026-07-25T15:33:57.892908Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n keras.callbacks.ModelCheckpoint(\n filepath=\"fine_tuning.keras\",\n save_best_only=True,\n monitor=\"val_loss\")\n]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:57.894752Z","iopub.execute_input":"2026-07-25T15:33:57.895048Z","iopub.status.idle":"2026-07-25T15:33:57.899031Z","shell.execute_reply.started":"2026-07-25T15:33:57.895026Z","shell.execute_reply":"2026-07-25T15:33:57.898408Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Fine-tune training (30 epochs)","metadata":{}},{"cell_type":"code","source":"history = model.fit(\n train_dataset,\n epochs=30,\n validation_data=validation_dataset,\n callbacks=callbacks)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:33:57.899941Z","iopub.execute_input":"2026-07-25T15:33:57.900218Z","iopub.status.idle":"2026-07-25T15:36:36.521543Z","shell.execute_reply.started":"2026-07-25T15:33:57.900187Z","shell.execute_reply":"2026-07-25T15:36:36.520786Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Final test accuracy — Fine-Tuned ModelThis should be the best-performing model in the whole notebook, since it combines pretrained VGG16 features + task-specific fine-tuning + data augmentation.## Summary| Approach | Typical Test Accuracy (from literature) ||---|---|| CNN from scratch | ~70% || CNN + Data Augmentation | ~80% || VGG16 Feature Extraction | ~97% || VGG16 + Augmentation (frozen) | ~98% || VGG16 Fine-Tuned | ~98%+ |(Actual numbers will depend on your run — this is the general trend that shows the benefit of transfer learning.)","metadata":{}},{"cell_type":"code","source":"model = keras.models.load_model(\"fine_tuning.keras\")\ntest_loss, test_acc = model.evaluate(test_dataset)\nprint(f\"Test accuracy: {test_acc:.3f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T15:36:36.522529Z","iopub.execute_input":"2026-07-25T15:36:36.522850Z","iopub.status.idle":"2026-07-25T15:36:40.425495Z","shell.execute_reply.started":"2026-07-25T15:36:36.522827Z","shell.execute_reply":"2026-07-25T15:36:40.424950Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}