{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## **Dog vs Cat Classifer**","metadata":{}},{"cell_type":"markdown","source":"### **Objective**\n\nIn this project, we are building a model to **classify images of dogs vs. cats**. This is a binary classification task where the model will learn to differentiate between two common animals: dogs and cats, based solely on their images. Given the variety of poses, breeds, and backgrounds, the model will need to focus on key visual features that can help distinguish between the two classes.\n\nThis is a classic example of how deep learning can be applied to simple yet meaningful image recognition tasks, helping to automate decisions that would otherwise require human judgment.\n\n---\n\n### **Data**\n\nFor this project, we will be using the **[Dogs vs. Cats](https://www.kaggle.com/competitions/dogs-vs-cats/data)** dataset. It consists of:\n\n* A **training set** of **25,000 labeled images** of dogs and cats, each image clearly labeled as either a dog or a cat.\n* A **test set** of **12,500 unlabeled images**, which will be used to evaluate the model's performance and generalization to new data.\n\nThis dataset gives us a solid foundation to build a classifier that can learn from a diverse set of images, making it ideal for training a model capable of accurately identifying whether an image contains a dog or a cat.\n","metadata":{}},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Imports**\n\nBefore diving into data exploration or model building, we will start by importing all the necessary libraries. These include tools for **data handling**, **image processing**, **deep learning model development**, and **performance evaluation**.\n\nSetting up these imports ensures that our environment is ready and that we have everything we need to move seamlessly through the next stages of the project.\n","metadata":{}},{"cell_type":"code","source":"# Downgrading protobuf to avoid: \"AttributeError: 'MessageFactory' object has no attribute 'GetPrototype'\" error\n!pip install protobuf==3.20.3 -q --no-warn-conflicts","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:33:57.048789Z","iopub.execute_input":"2025-12-16T14:33:57.049126Z","iopub.status.idle":"2025-12-16T14:34:00.736500Z","shell.execute_reply.started":"2025-12-16T14:33:57.049102Z","shell.execute_reply":"2025-12-16T14:34:00.735470Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# For filtering warnings\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n# Standard Python Libraries\nimport os\nimport zipfile\nimport datetime\nfrom tqdm import tqdm\n\n# Data Handling & Visualisation\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Machine Learning\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import confusion_matrix\n\n# Deep Learning\nimport tensorflow as tf","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:00.738243Z","iopub.execute_input":"2025-12-16T14:34:00.738795Z","iopub.status.idle":"2025-12-16T14:34:19.063001Z","shell.execute_reply.started":"2025-12-16T14:34:00.738758Z","shell.execute_reply":"2025-12-16T14:34:19.062038Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Project Configuration**\n\nBefore we start processing data or training our model, it’s useful to define all the key configurations in one place. This provides a clear foundation and makes the project easier to manage.\n\n* **Image parameters:** Defining input size, number of channels, and other image-related settings to be used throughout preprocessing and model training.\n* **Training constants:** Setting batch size, number of epochs, learning rate, and other hyperparameters for model training.\n* **Directory paths:** Organizing references to training and test folders, label files, and other resources to make data loading and management straightforward.\n\nBy centralizing these settings, we keep the project **organized, flexible, and easy to update**, which is especially helpful when experimenting with different models or hyperparameters.\n","metadata":{}},{"cell_type":"code","source":"# Configurations\nconfig = {\n\n    # Data\n    \"CLASS_INT_LABELS\": None,\n    \"CLASS_NAMES\": None,\n    \"TARGET_COLUMN\": \"encoded_label\",\n    \"FEATURE_COLUMN\": \"image_path\",\n\n    # File paths\n    \"DATA_DIR\": \"/kaggle/input/dogs-vs-cats\",\n    \"SAVE_MODEL_TO\": \"/kaggle/working/saved-models/\",\n    \"EXTRACT_TO\": \"/kaggle/working/\",\n    \"PROJECT_NAME\": \"dogs-vs-cats-classifier\",\n\n    # Dataset\n    \"TRAIN_RATIO\": 0.80,\n    \"RANDOM_STATE\": 7,\n    \"N_SAMPLES_TO_USE\": 8000,\n\n    # Model Architecture\n    \"IMG_SIZE\": 224,\n    \"N_CHANNELS\": 3,\n\n    # Regularization\n    \"DROPOUT_RATE\": 0.4,\n    \"L2_REG\": 1e-4,\n    \"OUTPUT_ACTIVATION_FUNC\": \"sigmoid\",\n\n    # CNN Config\n    \"INITIAL_N_FILTERS\": 32,\n    \"KERNEL_SIZE\": (3, 3),\n    \"PADDING\": \"same\",\n    \"KERNEL_INIT\": \"he_normal\",\n\n    # Dense Layer\n    \"DENSE_UNITS\": 128,\n\n    # Training\n    \"BATCH_SIZE\": 32,\n    \"BUFFER_SIZE\": 500,\n    \"LEARNING_RATE\": 1e-3,\n    \"N_EPOCHS\": 100,\n\n    # Callback Parameters\n    \"METRIC_TO_MONITOR\": \"val_accuracy\",\n    \"ES_PATIENCE\": 10,\n    \"LR_PATIENCE\": 3,\n    \"MIN_DELTA\": 0.001,\n    \"LR_FACTOR\": 0.5,\n    \"MIN_LR\": 1e-7,\n    \"RESTORE_BEST_WEIGHTS\": True\n\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:19.064028Z","iopub.execute_input":"2025-12-16T14:34:19.065008Z","iopub.status.idle":"2025-12-16T14:34:19.070577Z","shell.execute_reply.started":"2025-12-16T14:34:19.064976Z","shell.execute_reply":"2025-12-16T14:34:19.069732Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Extracting Dataset Files**\n\nWith our environment set up, the next step is to **extract the dataset files**. This ensures that all dataset files are correctly organized and ready for preprocessing and training. \n\nWe will create a function to extract all files from a ZIP file into the specified directory. This function will handles the creation of the output directory and reads the ZIP file to extract its contents. By extracting the files, we ensure that our dataset is properly structured and ready for the next steps in preprocessing and model training.\n","metadata":{}},{"cell_type":"code","source":"# Function to extract all files from a ZIP file into the target directory\ndef extract_zip_files(zip_file_name, config=config):\n    \"\"\"\n    Extract all files from the given ZIP file into a specified directory.\n\n    Parameters:\n    - zip_file_name: Name of the ZIP file.\n    - config: Configuration dictionary.\n\n    Returns: None\n    \"\"\"\n\n    # Creating the output directory if it doesn't exist\n    os.makedirs(config.get(\"EXTRACT_TO\", \"\"), exist_ok=True)\n\n    # Getting the full path to the ZIP file\n    zip_path = os.path.join(config.get(\"DATA_DIR\", \"\"), zip_file_name)\n\n    # Opening and reading the ZIP file\n    with zipfile.ZipFile(zip_path, \"r\") as zip_ref:\n\n        # List of all files inside the ZIP\n        file_list = zip_ref.infolist()\n\n        # Extracting files with a progress bar\n        for file in tqdm(file_list, desc=f\"Extracting {zip_file_name.title()}\", unit=\"file\"):\n            zip_ref.extract(member=file, path=config.get(\"EXTRACT_TO\", \"\"))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:19.073270Z","iopub.execute_input":"2025-12-16T14:34:19.073522Z","iopub.status.idle":"2025-12-16T14:34:19.105373Z","shell.execute_reply.started":"2025-12-16T14:34:19.073504Z","shell.execute_reply":"2025-12-16T14:34:19.104350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Unzipping the data\nextract_zip_files(\"train.zip\")\nextract_zip_files(\"test1.zip\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:19.106316Z","iopub.execute_input":"2025-12-16T14:34:19.106605Z","iopub.status.idle":"2025-12-16T14:34:36.265295Z","shell.execute_reply.started":"2025-12-16T14:34:19.106582Z","shell.execute_reply":"2025-12-16T14:34:36.264546Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Loading & Exploring the Dataset**\n\nNow that we have extracted the files, the next step is to load the dataset into a structured format so we can inspect it before moving forward. This helps us confirm that the files were extracted correctly and that everything is organized the way we need for preprocessing or training.\n\n* **Load image paths and labels**: Create a function to gather all file paths from the dataset folders and assign labels to the training images so we know which ones are dogs and which ones are cats.\n* **Check for duplicates**: Scan for repeated images to make sure the model isn’t learning from the same sample multiple times.\n* **Review class balance**: Count how many dog and cat images we have to confirm the dataset is balanced.\n* **Validate file counts**: Ensure the number of images we load matches the number of actual files in each directory.\n\nThis quick inspection ensures the dataset is clean, complete, and ready for preprocessing and model training.\n","metadata":{}},{"cell_type":"code","source":"# Function to load data from folders and generate file paths for images\ndef load_data(dataset_type, config):\n    \"\"\"\n    Loads image file paths for the given dataset type.\n\n    Parameters:\n    - dataset_type : One of 'train', 'training', 'test', 'testing'.\n    - config : Configuration dictionary.\n\n    Returns:\n    - pd.DataFrame: DataFrame with image paths and labels (for training data).\n    \"\"\"\n\n    # Normalizing dataset type\n    ds_type = dataset_type.lower()\n\n    # Validating dataset_type\n    if ds_type not in (\"train\", \"training\", \"test\", \"testing\"):\n        raise ValueError(\"Invalid 'dataset_type' provided. It should be one of ('train', 'training', 'test', 'testing').\")\n\n    # Getting the full path to the dataset folder\n    base_dir = config.get(\"EXTRACT_TO\", \"\")\n    folder_path = next(\n        (os.path.join(base_dir, folder) for folder in [ds_type, ds_type + \"1\"] if os.path.exists(os.path.join(base_dir, folder))),\n        None\n    )\n\n    # Checking if a valid dataset folder was found\n    if folder_path is None:\n        raise FileNotFoundError(f\"No valid folder found for dataset_type='{dataset_type}' under '{base_dir}'.\")\n\n    # List of all image filenames (ignoring hidden/system files)\n    filenames = [f for f in os.listdir(folder_path) if not f.startswith(\".\")]\n\n    # Building DataFrame with image file paths\n    feature_col = config.get(\"FEATURE_COLUMN\", \"path\")\n    image_paths = [os.path.join(folder_path, f) for f in filenames]\n    df = pd.DataFrame({feature_col: image_paths})\n\n    # Adding labels for training data\n    if ds_type.startswith(\"train\"):\n        df[\"label\"] = [fname.split(\".\")[0] for fname in filenames]\n\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.266148Z","iopub.execute_input":"2025-12-16T14:34:36.266429Z","iopub.status.idle":"2025-12-16T14:34:36.273481Z","shell.execute_reply.started":"2025-12-16T14:34:36.266408Z","shell.execute_reply":"2025-12-16T14:34:36.272789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Loading training data\ntrain_df = load_data(\"train\", config)\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.274446Z","iopub.execute_input":"2025-12-16T14:34:36.274791Z","iopub.status.idle":"2025-12-16T14:34:36.356736Z","shell.execute_reply.started":"2025-12-16T14:34:36.274764Z","shell.execute_reply":"2025-12-16T14:34:36.355859Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Loading testing data\ntest_df = load_data(\"test\", config)\ntest_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.357797Z","iopub.execute_input":"2025-12-16T14:34:36.358135Z","iopub.status.idle":"2025-12-16T14:34:36.386756Z","shell.execute_reply.started":"2025-12-16T14:34:36.358107Z","shell.execute_reply":"2025-12-16T14:34:36.385993Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Checking for duplicate values in the datasets\nprint(f\"{'Training Dataset':>16}: {train_df.duplicated().sum()} duplicates\")\nprint(f\"{'Testing Dataset':>16}: {test_df.duplicated().sum()} duplicates\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.387706Z","iopub.execute_input":"2025-12-16T14:34:36.388484Z","iopub.status.idle":"2025-12-16T14:34:36.408758Z","shell.execute_reply.started":"2025-12-16T14:34:36.388460Z","shell.execute_reply":"2025-12-16T14:34:36.408021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Checking the distribution of dog and cat images in the training dataset\ntrain_df[\"label\"].value_counts(normalize = True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.411569Z","iopub.execute_input":"2025-12-16T14:34:36.411842Z","iopub.status.idle":"2025-12-16T14:34:36.422251Z","shell.execute_reply.started":"2025-12-16T14:34:36.411825Z","shell.execute_reply":"2025-12-16T14:34:36.421360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Checking if the number of image paths matches the number of actual image files\nif len(os.listdir(os.path.join(config.get(\"EXTRACT_TO\", \"\"), \"train\"))) == len(train_df[\"image_path\"]):\n    print(\"File check passed: Number of filenames matches the number of image files. Good to proceed.\")\nelse:\n    print(\"File check failed: Mismatch between filenames and image files. Please verify.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.422969Z","iopub.execute_input":"2025-12-16T14:34:36.423224Z","iopub.status.idle":"2025-12-16T14:34:36.455786Z","shell.execute_reply.started":"2025-12-16T14:34:36.423195Z","shell.execute_reply":"2025-12-16T14:34:36.455015Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Visualizing Sample Images**\n\nNow that the dataset is loaded and checked, the next step is to take a quick look at some of the actual images. This gives us a clearer sense of what the model will see during training and helps confirm that the images were read correctly.\n\nWe will use a small helper function to display the **first 25 images** from the training dataset in a clean **5×5 grid**, giving us a quick visual check of what the model will be learning from. This helps us confirm that the images are loading correctly, are properly formatted, and includes clear examples of both dogs and cats. \n\nBy looking at these samples upfront, we can make sure the dataset is in good shape and free from any unexpected issues before moving forward.","metadata":{}},{"cell_type":"code","source":"# Function to display the first 25 images\ndef display_25_images(images, labels):\n    \"\"\"\n    Displays the first 25 images from the provided list of image file paths.\n\n    Parameters:\n    - images: List of image file paths.\n    - labels: List of image labels.\n\n    Returns: None\n    \"\"\"\n\n    # Setting figure size\n    plt.figure(figsize=(15, 15))\n\n    # Looping through the images\n    for i in range(25):\n\n        # Plotting the image in a 5x5 grid\n        plt.subplot(5, 5, i + 1)\n        plt.imshow(plt.imread(images[i]) / 255.0)\n        plt.title(f\"{labels[i].title()}\")\n\n        # Hiding axis and adjusting layout\n        plt.axis(\"off\")\n        plt.tight_layout()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.456607Z","iopub.execute_input":"2025-12-16T14:34:36.456886Z","iopub.status.idle":"2025-12-16T14:34:36.462460Z","shell.execute_reply.started":"2025-12-16T14:34:36.456860Z","shell.execute_reply":"2025-12-16T14:34:36.461666Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Displaying the first 25 images from the training dataset\ndisplay_25_images(train_df[\"image_path\"], train_df[\"label\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:36.463217Z","iopub.execute_input":"2025-12-16T14:34:36.463428Z","iopub.status.idle":"2025-12-16T14:34:42.680866Z","shell.execute_reply.started":"2025-12-16T14:34:36.463411Z","shell.execute_reply":"2025-12-16T14:34:42.679923Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Encoding Labels**\n\nBefore training the model, it’s important to **encode the labels** into a numerical format, since machine learning models can only work with numbers and not text. \n\nWe use the **LabelEncoder** to transform the categorical labels in the training dataset: \"dog\" and \"cat\" into integer values that the model can process for classification. At the same time, we store the **class names** and their corresponding **integer labels** in the configuration, making it easy to reference them during training and evaluation. \n\nBy encoding the labels this way, we ensure the model can properly interpret the target data and learn to distinguish between the two classes effectively.","metadata":{}},{"cell_type":"code","source":"# Creating an instance of LabelEncoder\nencoder = LabelEncoder()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.681911Z","iopub.execute_input":"2025-12-16T14:34:42.682216Z","iopub.status.idle":"2025-12-16T14:34:42.686534Z","shell.execute_reply.started":"2025-12-16T14:34:42.682188Z","shell.execute_reply":"2025-12-16T14:34:42.685252Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Encoding output classes for training\ntrain_df[config.get(\"TARGET_COLUMN\", \"\")] = encoder.fit_transform(train_df[\"label\"])\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.687846Z","iopub.execute_input":"2025-12-16T14:34:42.688270Z","iopub.status.idle":"2025-12-16T14:34:42.716743Z","shell.execute_reply.started":"2025-12-16T14:34:42.688214Z","shell.execute_reply":"2025-12-16T14:34:42.715970Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Setting \"CLASS_NAMES\" & \"CLASS_INT_LABELS\"\nconfig[\"CLASS_INT_LABELS\"] = train_df[config.get(\"TARGET_COLUMN\", \"\")].sort_values().unique()\nconfig[\"CLASS_NAMES\"] = encoder.inverse_transform(config.get(\"CLASS_INT_LABELS\", []))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.717476Z","iopub.execute_input":"2025-12-16T14:34:42.717719Z","iopub.status.idle":"2025-12-16T14:34:42.734625Z","shell.execute_reply.started":"2025-12-16T14:34:42.717692Z","shell.execute_reply":"2025-12-16T14:34:42.733767Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Data Preprocessing**\n\nBefore feeding images into the model, it’s important to preprocess them so they are in a consistent and usable format. To handle this systematically, we create a function that\n\n* **Read the image file**: Load the raw image data from its file path into memory.\n* **Decode the JPEG contents**: Convert the image into a tensor with the correct number of channels for the model.\n* **Resize the image**: Adjust all images to a fixed height and width so every sample matches the expected input shape.\n* **Return image and label when needed**: Provide both image and label for training and validation datasets, while returning only the image for test datasets.\n* **Check the dataset type**: Ensure labels are included only where appropriate, avoiding any mix-ups between training, validation, and test data.\n\nThis preprocessing ensures that all images are standardized, giving the model clean, consistent inputs for smoother training and evaluation.","metadata":{}},{"cell_type":"code","source":"# Function to preprocess images\ndef preprocess_images(filepath, label = None, dataset_type = \"train\"):\n    \"\"\"\n    Preprocesses (read, decode, and resize) an image.\n\n    Parameters:\n    - filepath: Path to the image file.\n    - label: Label corresponding to the image.\n    - dataset_type: The dataset type (\"train\", \"validation\", etc.).\n\n    Returns:\n    - tf.Tensor: Preprocessed image tensor, and the label if dataset_type is \"train\" or \"validation\".\n    \"\"\"\n\n    # Reading the image file\n    img = tf.io.read_file(filepath)\n\n    # Decoding the JPEG image\n    img = tf.image.decode_jpeg(img, channels=config.get(\"N_CHANNELS\", 3))\n\n    # Resizing the image\n    img = tf.image.resize(img, [config.get(\"IMG_SIZE\", 224), config.get(\"IMG_SIZE\", 224)])\n\n    # Returning image-label pair for training/validation datasets\n    if dataset_type.startswith((\"train\", \"training\", \"val\", \"validation\")):\n        if label is None:\n            raise ValueError(\n                f\"For dataset type '{dataset_type}', labels must be provided.\"\n            )\n        return (img, label)\n\n    elif dataset_type.startswith((\"test\", \"testing\")):\n        if label is not None:\n            raise ValueError(\n                f\"For dataset type '{dataset_type}', no labels should be provided.\"\n            )\n        return img\n\n    else:\n        raise ValueError(\n            f\"Invalid datasety_type='{dataset_type}'. Expected 'train', 'val', or 'test'.\"\n        )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.735879Z","iopub.execute_input":"2025-12-16T14:34:42.736246Z","iopub.status.idle":"2025-12-16T14:34:42.750155Z","shell.execute_reply.started":"2025-12-16T14:34:42.736197Z","shell.execute_reply":"2025-12-16T14:34:42.749232Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Creating Dataset**\n\nAfter preprocessing the images, the next step is to organize them efficiently so the model can access them in manageable batches during training. This ensures smooth training and avoids memory overload when working with large datasets.\n\n* **Split into batches:** Group the images into smaller batches (for example, 32 images per batch) so the model can process the data efficiently and update weights frequently.\n* **Pair images with labels:** Combine each preprocessed image with its corresponding label to create (image, label) pairs that the model can use for learning.\n* **Build a dataset function:** Create a function that takes image paths and labels, applies preprocessing, and outputs a ready-to-use TensorFlow dataset.\n* **Shuffle training data:** Randomize the order of training batches to help the model learn general patterns and prevent overfitting.\n* **Prepare training and validation sets:** Apply the same dataset creation process to both splits to ensure consistent preprocessing, batching, and handling of data for evaluation.\n\nBy creating these structured datasets, we set up a memory-efficient and well-organized pipeline, allowing the model to train effectively on consistent, manageable chunks of data.","metadata":{}},{"cell_type":"code","source":"# Splitting data into X and y\nX = train_df[config.get(\"FEATURE_COLUMN\", \"\")]\ny = train_df[config.get(\"TARGET_COLUMN\", \"\")]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.750953Z","iopub.execute_input":"2025-12-16T14:34:42.751286Z","iopub.status.idle":"2025-12-16T14:34:42.768951Z","shell.execute_reply.started":"2025-12-16T14:34:42.751267Z","shell.execute_reply":"2025-12-16T14:34:42.767998Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to create dataset\ndef create_dataset(X, y=None, config=config, dataset_type=\"train\", shuffle=True):\n    \"\"\"\n    Creates a TensorFlow dataset for training, validation, or testing.\n\n    Parameters:\n    - X: List or array of image file paths.\n    - y: Labels corresponding to X.\n    - config: Configuration dictionary.\n    - dataset_type: One of \"train\", \"val\", or \"test\".\n    - shuffle: Whether to shuffle the dataset (only applies to training).\n\n    Returns:\n    - tf.data.Dataset: A TensorFlow dataset object.\n    \"\"\"\n\n    # Normalzing dataset_type\n    ds_type = dataset_type.lower()\n    print(f\"Creating {ds_type.title()} Dataset...\")\n\n    # Training & Validation Dataset\n    if ds_type.startswith((\"train\", \"training\", \"val\", \"validation\")):\n        if y is None:\n            raise ValueError(\n                f\"For dataset type '{ds_type}', labels must be provided.\"\n            )\n\n        dataset = tf.data.Dataset.from_tensor_slices((X, y))\n        dataset = dataset.map(\n            lambda img_path, label: preprocess_images(img_path, label, ds_type),\n            num_parallel_calls=tf.data.AUTOTUNE,\n            deterministic=True\n        )\n\n        # Shuffling the dataset for training\n        if shuffle and ds_type.startswith((\"train\", \"training\")):\n            dataset = dataset.shuffle(\n                buffer_size=config.get(\"BUFFER_SIZE\", 500),\n                reshuffle_each_iteration=True,\n                seed=config.get(\"RANDOM_STATE\", 7)\n            )\n\n    # Testing Dataset\n    elif ds_type.startswith((\"test\", \"testing\")):\n        if y is not None:\n            raise ValueError(\n                f\"For dataset type '{ds_type}', no label should be provided.\"\n            )\n        dataset = tf.data.Dataset.from_tensor_slices(X)\n        dataset = dataset.map(\n            lambda img_path: preprocess_images(img_path, dataset_type=ds_type),\n            num_parallel_calls=tf.data.AUTOTUNE,\n            deterministic=True\n        )\n\n    else:\n        raise ValueError(\n            f\"Invalid dataset_type='{ds_type}'. Expected 'train', 'validation', or 'test'.\"\n        )\n\n    # Batching, and Prefetching\n    dataset = dataset.batch(batch_size=config.get(\"BATCH_SIZE\", 32)).prefetch(tf.data.AUTOTUNE)\n\n    return dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.769949Z","iopub.execute_input":"2025-12-16T14:34:42.770642Z","iopub.status.idle":"2025-12-16T14:34:42.788723Z","shell.execute_reply.started":"2025-12-16T14:34:42.770619Z","shell.execute_reply":"2025-12-16T14:34:42.787947Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to split the data into train and validation sets to create tf.data.Dataset\ndef prepare_train_val_datasets(X, y, config=config):\n    \"\"\"\n    Splits the data into training and validation sets, then creates TensorFlow datasets for model training and evaluation.\n\n    Parameters:\n    - X: Array of image file paths.\n    - y: Array of corresponding labels for each image.\n    - config: Configuration dictionary.\n\n    Returns:\n    - tuple: (train_dataset, val_dataset)\n    \"\"\"\n\n    # Train-Validation split\n    X_train, X_val, y_train, y_val = train_test_split(\n        X,\n        y,\n        train_size = config.get(\"TRAIN_RATIO\", 0.80),\n        random_state = config.get(\"RANDOM_STATE\", 7),\n        stratify = y  # To ensure the class distribution\n    )\n\n    # Print dataset sizes\n    print(f\"# Dataset sizes:\")\n    print(f\"1. {'Training':<12}: {X_train.shape, y_train.shape}\")\n    print(f\"2. {'Validation':<12}: {X_val.shape, y_val.shape}\")\n\n    # Creating TensorFlow dataset for the training set\n    train_dataset = create_dataset(\n        X = X_train,\n        y = y_train,\n        config = config,\n        dataset_type = \"train\",\n        shuffle = True\n    )\n\n    # Create TensorFlow dataset for the validation set\n    val_dataset = create_dataset(\n        X = X_val,\n        y = y_val,\n        config = config,\n        dataset_type = \"val\",\n        shuffle = False\n    )\n\n    return train_dataset, val_dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.789660Z","iopub.execute_input":"2025-12-16T14:34:42.790279Z","iopub.status.idle":"2025-12-16T14:34:42.810459Z","shell.execute_reply.started":"2025-12-16T14:34:42.790251Z","shell.execute_reply":"2025-12-16T14:34:42.809376Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Defining Model Architecture**\n\nWith our data pipeline ready and images preprocessed, the next step is to design the model that will actually learn to distinguish dogs from cats. Here, we focus on building a flexible yet powerful structure capable of extracting meaningful features from the images and turning them into accurate predictions.\n\n* **Set up the input layer:** Define the shape of the images the model will receive so it knows exactly what to expect.\n* **Apply data augmentation:** Introduce random flips, rotations, zooms, and other transformations to help the model generalize better to new images.\n* **Rescale pixel values:** Normalize the image data to a consistent scale, which stabilizes and speeds up training.\n* **Build the feature extractor:** Construct either a VGG-style model or a ResNet-style block that can capture important visual patterns and hierarchical features from the images.\n* **Add the classification head:** Transform the extracted features into a final output using pooling, dense layers, and a sigmoid activation for binary classification.\n* **Compile the model:** Set the optimizer, loss function, and evaluation metrics to make the model ready for training.\n\nThis step gives us a well-structured and versatile model that can efficiently learn from our dataset, adapt to different architectures, and provide reliable predictions for distinguishing between dogs and cats.","metadata":{}},{"cell_type":"code","source":"# Function to build a custom VGG model\ndef build_vgg_model(inputs):\n    \"\"\"\n    Builds a VGG-Style feature extraction layers.\n\n    Parameters:\n    - inputs: Input tensor.\n\n    Returns:\n    - Tensor after feature extraction.\n    \"\"\"\n\n    # Conv Block: 1 (Stride = 2 for downsampling)\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 1,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n        strides=2,\n    )(inputs)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 1,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    # MaxPooling Layer: 1\n    x = tf.keras.layers.MaxPooling2D((2, 2))(x)\n\n    \n    # Conv Block: 2\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 2,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 2,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    # MaxPooling Layer: 2\n    x = tf.keras.layers.MaxPooling2D((2, 2))(x)\n\n\n    # Conv Block: 3\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 4,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 4,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    # MaxPooling Layer: 3\n    x = tf.keras.layers.MaxPooling2D((2, 2))(x)\n\n    \n    # Conv Block: 4\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 8,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    x = tf.keras.layers.Conv2D(\n        filters=config.get(\"INITIAL_N_FILTERS\", 32) * 8,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3, 3)),\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Activation(\"relu\")(x)\n\n    # MaxPooling Layer: 4\n    x = tf.keras.layers.MaxPooling2D((2, 2))(x)\n    \n    # Dropout Layer\n    x = tf.keras.layers.Dropout(rate = config.get(\"DROPOUT_RATE\", 0.4))(x)\n\n    return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.811508Z","iopub.execute_input":"2025-12-16T14:34:42.811828Z","iopub.status.idle":"2025-12-16T14:34:42.835094Z","shell.execute_reply.started":"2025-12-16T14:34:42.811802Z","shell.execute_reply":"2025-12-16T14:34:42.834138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to create residual block for ResNet model\ndef residual_block(x, filters, stride=1, config=config):\n    \"\"\"\n    Builds a residual block with optional downsampling.\n\n    Parameters:\n    - x: Input tensor.\n    - filters: Number of filters for conv layers.\n    - stride: Stride for downsampling.\n    - config: Configuration dictionary.\n\n    Returns:\n    - Output tensor of the residual block.\n    \"\"\"\n\n    # Saving input for shortcut\n    shortcut = x\n\n    # Conv Block: 1\n    x = tf.keras.layers.Conv2D(\n        filters=filters,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3,3)),\n        strides=stride,\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.ReLU()(x)\n\n    # Conv Block: 2\n    x = tf.keras.layers.Conv2D(\n        filters=filters,\n        kernel_size=config.get(\"KERNEL_SIZE\", (3,3)),\n        strides=1,\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n\n    # Matching Shortcut Dimensions in case of shape mismatch\n    if stride != 1 or shortcut.shape[-1] != filters:\n        shortcut = tf.keras.layers.Conv2D(\n            filters=filters,\n            kernel_size=(1, 1),\n            strides=stride,\n            padding=config.get(\"PADDING\", \"same\"),\n            kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n        )(shortcut)\n        shortcut = tf.keras.layers.BatchNormalization()(shortcut)\n\n    # Adding & Activating\n    x = tf.keras.layers.Add()([x, shortcut])\n    x = tf.keras.layers.ReLU()(x)\n\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.836124Z","iopub.execute_input":"2025-12-16T14:34:42.836467Z","iopub.status.idle":"2025-12-16T14:34:42.856275Z","shell.execute_reply.started":"2025-12-16T14:34:42.836441Z","shell.execute_reply":"2025-12-16T14:34:42.855335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to build a ResNet-Style model\ndef build_resnet_model(inputs):\n    \"\"\"\n    Builds a ResNet-style feature extractor.\n\n    Parameters:\n    - inputs: Input tensor.\n\n    Returns:\n    - Output tensor after the ResNet feature extractor.\n\n    \"\"\"\n\n    # Stem Block: Initial Conv, BatchNormalisation, ReLU, and Downsampling\n    x = tf.keras.layers.Conv2D(\n        filters=64,\n        kernel_size=(5, 5),\n        strides=2,\n        padding=config.get(\"PADDING\", \"same\"),\n        kernel_initializer=config.get(\"KERNEL_INIT\", \"he_normal\"),\n    )(inputs)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.ReLU()(x)\n    x = tf.keras.layers.MaxPooling2D(pool_size=3, strides=2, padding=config.get(\"PADDING\", \"same\"))(x)\n\n    # Residual Stack\n    filter_sizes = [64, 128, 256, 512]\n    num_blocks = [2, 2, 2, 2]\n    \n    for i, (n_filters, n_blocks) in enumerate(zip(filter_sizes, num_blocks)):\n        for block_idx in range(n_blocks):\n            # Downsampling at the first block of each stage (except the first stage)\n            stride = 2 if (i > 0 and block_idx == 0) else 1\n            x = residual_block(x, filters=n_filters, stride=stride)\n\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.857276Z","iopub.execute_input":"2025-12-16T14:34:42.857499Z","iopub.status.idle":"2025-12-16T14:34:42.880869Z","shell.execute_reply.started":"2025-12-16T14:34:42.857482Z","shell.execute_reply":"2025-12-16T14:34:42.880119Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to build a classification head\ndef build_classification_head(features, config=config):\n    \"\"\"\n    Builds the classification head on top of extracted features.\n\n    Parameters:\n    - features: Output tensor from the feature extractor.\n    - config: Configuration dictionary.\n\n    Returns:\n    - Final prediction tensor.\n    \"\"\"\n\n    # Getting the features\n    x = features\n\n    # Applying global pooling\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    x = tf.keras.layers.Dropout(config.get(\"DROPOUT_RATE\", 0.4))(x)\n\n\n    # Dense Block\n    x = tf.keras.layers.Dense(\n        units = config.get(\"DENSE_UNITS\", 128),\n        kernel_initializer = config.get(\"KERNEL_INIT\", \"he_normal\"),\n        kernel_regularizer = tf.keras.regularizers.l2(config.get(\"L2_REG\", 1e-4))\n    )(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.ReLU()(x)\n    x = tf.keras.layers.Dropout(config.get(\"DROPOUT_RATE\", 0.4))(x)\n\n    # Output layer\n    outputs = tf.keras.layers.Dense(\n        units = 1,\n        activation = config.get(\"OUTPUT_ACTIVATION_FUNC\", \"sigmoid\"),\n        name = \"output\"\n    )(x)\n\n    return outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.881817Z","iopub.execute_input":"2025-12-16T14:34:42.882683Z","iopub.status.idle":"2025-12-16T14:34:42.901371Z","shell.execute_reply.started":"2025-12-16T14:34:42.882653Z","shell.execute_reply":"2025-12-16T14:34:42.900259Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to create the final model\ndef create_model(model_name=\"vgg\", config=config):\n    \"\"\"\n    Builds and compiles a complete model.\n\n    Parameters:\n    - model_name: The base model to use (\"vgg\" or \"resnet\").\n    - config: Configuration dictionary.\n\n    Returns:\n    - tf.keras.Model: Compiled model ready for training.\n    \"\"\"\n\n    print(f\"\\n{'='*60}\")\n    print(f\"CREATING {model_name.upper()} MODEL\")\n    print(f\"{'='*60}\")\n\n    # Normalizing model_name\n    model_name = model_name.lower()\n\n    # Input shape\n    input_shape = (\n        config.get(\"IMG_SIZE\", 224),\n        config.get(\"IMG_SIZE\", 224),\n        config.get(\"N_CHANNELS\", 3)\n    )\n\n    # Input layer\n    inputs = tf.keras.layers.Input(shape = input_shape)\n\n    # Data augmentation\n    augmentation = tf.keras.Sequential([\n        tf.keras.layers.RandomFlip(\"horizontal\", seed = config.get(\"RANDOM_STATE\", 7)),          # Random horizontal flip\n        tf.keras.layers.RandomRotation(0.15, seed = config.get(\"RANDOM_STATE\", 7)),              # Random rotation\n        tf.keras.layers.RandomZoom(0.1, seed = config.get(\"RANDOM_STATE\", 7)),                   # Random zoom\n        tf.keras.layers.RandomTranslation(0.1, 0.1, seed=config.get(\"RANDOM_STATE\", 7)),         # Random translation\n        tf.keras.layers.RandomContrast(0.15, seed=config.get(\"RANDOM_STATE\", 7)),                # Random contrast\n        tf.keras.layers.RandomBrightness(0.15, seed=config.get(\"RANDOM_STATE\", 7)),              # Random brightness\n    ], name=\"augmentation\")\n    x = augmentation(inputs)\n\n    # Rescale pixel values\n    x = tf.keras.layers.Rescaling(1./255)(x)\n\n    # Building the base model\n    if model_name.startswith((\"vgg\")):\n        base_output = build_vgg_model(inputs = x)\n\n    elif model_name.startswith(\"resnet\"):\n        base_output = build_resnet_model(inputs = x)\n        \n    else:\n        raise ValueError(f\"Unknown model name: {model_name}. Expected one of ('vgg', or 'resnet').\")\n\n    # Adding the classification head\n    outputs = build_classification_head(features = base_output, config = config)\n\n    # Creating the complete model\n    model = tf.keras.Model(\n        inputs = inputs,\n        outputs = outputs,\n        name = f\"{model_name}_classifier\"\n    )\n\n    # Compiling the model\n    model.compile(\n        optimizer = tf.keras.optimizers.Adam(\n            learning_rate = config.get(\"LEARNING_RATE\", 0.001)\n        ),\n        loss = tf.keras.losses.BinaryCrossentropy(),\n        metrics=[\n            \"accuracy\",\n            tf.keras.metrics.Precision(name = \"precision\"),\n            tf.keras.metrics.Recall(name = \"recall\"),\n            tf.keras.metrics.AUC(name = \"auc\")\n        ]\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.902504Z","iopub.execute_input":"2025-12-16T14:34:42.902922Z","iopub.status.idle":"2025-12-16T14:34:42.921592Z","shell.execute_reply.started":"2025-12-16T14:34:42.902895Z","shell.execute_reply":"2025-12-16T14:34:42.920863Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Creating Model Callbacks**\n\nWith the model architecture defined, the next step is to make the training process smarter and more efficient. This is where **callbacks** come into play. Callbacks are tools that monitor the training process and automatically adjust behavior to improve performance, save resources, and prevent overfitting.\n\nIn this step, we create two key callbacks. \n* The **Early Stopping callback** monitors the model’s performance on the validation set and stops training if no improvement is observed for a set number of epochs, preventing overfitting and unnecessary computation.\n* The **Reduce Learning Rate on Plateau callback** dynamically lowers the learning rate when the monitored metric stops improving, allowing the model to converge more smoothly and potentially reach a better final performance.\n\nBy integrating these callbacks, we ensure the model trains efficiently, adapts to learning challenges during training, and achieves better generalization on unseen data.","metadata":{}},{"cell_type":"code","source":"# Function to get the list of callbacks\ndef get_callbacks(config=config):\n\n    \"\"\"\n    Create and return a list of `tf.keras.callbacks` configured using the provided values.\n\n    Parameters\n    - config : Configuration dictionary.\n\n    Returns\n    - list: List of `tf.keras.callbacks` instances to use during model training.\n    \"\"\"\n\n    # List to store callbacks\n    callback_list = []\n\n    # EarlyStopping callback\n    early_stopping = tf.keras.callbacks.EarlyStopping(\n        monitor = config.get(\"METRIC_TO_MONITOR\", \"val_accuracy\"),\n        min_delta = config.get(\"MIN_DELTA\", 0.001),\n        patience = config.get(\"ES_PATIENCE\", 10),\n        verbose = 1,\n        restore_best_weights = config.get(\"RESTORE_BEST_WEIGHTS\", True),\n        mode='max'\n    )\n    callback_list.append(early_stopping)\n\n    # ReduceLROnPlateau callback\n    reduce_lr = tf.keras.callbacks.ReduceLROnPlateau(\n        monitor = config.get(\"METRIC_TO_MONITOR\", \"val_accuracy\"),\n        factor = config.get(\"LR_FACTOR\", 0.5),\n        patience = config.get(\"LR_PATIENCE\", 3),\n        verbose = 1,\n        min_delta = config.get(\"MIN_DELTA\", 0.001),\n        min_lr = config.get(\"MIN_LR\", 1e-7),\n        mode='max'\n    )\n    callback_list.append(reduce_lr)\n\n    return callback_list","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.922859Z","iopub.execute_input":"2025-12-16T14:34:42.923113Z","iopub.status.idle":"2025-12-16T14:34:42.943555Z","shell.execute_reply.started":"2025-12-16T14:34:42.923083Z","shell.execute_reply":"2025-12-16T14:34:42.942708Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Training a Model (Subset of Data)**\n\nBefore training on the full dataset, we will begin with a **smaller subset of 8,000 images** to quickly verify that the training pipeline is functioning correctly. \n\n* **Create and compile the model:** Use the selected architecture (VGG or ResNet) and prepare it for training.\n* **Train on the subset:** Fit the model on the smaller dataset for a set number of epochs, using the validation subset to monitor performance.\n* **Use callbacks:** Apply the previously defined callbacks like EarlyStopping and ReduceLROnPlateau to ensure training is efficient and prevents overfitting.\n* **Review training history:** Track metrics such as accuracy, loss, precision, recall, and AUC to confirm that the model is learning as expected.\n\nThis initial training step provides confidence that the pipeline works properly before scaling up to the full dataset and will also help us identify which model architecture is learning better and achieving higher validation accuracy on the smaller subset of images, so we can decide which model to use for training the final model on the full dataset.","metadata":{}},{"cell_type":"code","source":"# Function to train the model\ndef train_model(model_name, training_dataset, num_epochs, validation_dataset, callbacks):\n    \"\"\"\n    Creates and trains a model using the provided datasets and callbacks.\n\n    Parameters:\n    - model_name:  The base model to use (\"vgg\" or \"resnet\").\n    - training_dataset: Dataset for training the model.\n    - num_epochs: Number of epochs to train the model.\n    - validation_dataset: Dataset for validating the model during training.\n    - callbacks: List of callbacks to use during training.\n\n    Returns:\n    - tuple: (model, history)\n    \"\"\"\n\n    # Creating the full model\n    model = create_model(model_name = model_name)\n\n    # Training the model\n    history = model.fit(\n        training_dataset,\n        epochs = num_epochs,\n        validation_data = validation_dataset,\n        callbacks = callbacks,\n        verbose=1,\n    )\n\n    # Return the trained model\n    return model, history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.944468Z","iopub.execute_input":"2025-12-16T14:34:42.945032Z","iopub.status.idle":"2025-12-16T14:34:42.963427Z","shell.execute_reply.started":"2025-12-16T14:34:42.945009Z","shell.execute_reply":"2025-12-16T14:34:42.962606Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Training VGG Model**\n\nWe begin by training the `VGG-Style` model on a subset of the data to quickly evaluate its performance before scaling to the full dataset. \n\n* **Prepare training and validation datasets** using a limited number of samples to ensure the model receives a representative portion of the data.\n* Train the model using the **defined pipeline**, applying callbacks to monitor progress, prevent overfitting, and adjust the learning rate automatically.\n* Observe metrics such as **accuracy, loss, precision, recall, and AUC** on both training and validation sets to assess learning performance.\n\nThis step helps us identify whether the VGG architecture achieves satisfactory validation performance and based on this evaluation, decide if the **VGG model is suitable** for training on the full dataset.","metadata":{}},{"cell_type":"code","source":"# Creating training and validation datasets for VGG model\ntrain_dataset_vgg, val_dataset_vgg = prepare_train_val_datasets(\n    X = X[:config.get(\"N_SAMPLES_TO_USE\", 8000)],\n    y = y[:config.get(\"N_SAMPLES_TO_USE\", 8000)],\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:42.967283Z","iopub.execute_input":"2025-12-16T14:34:42.967594Z","iopub.status.idle":"2025-12-16T14:34:45.585826Z","shell.execute_reply.started":"2025-12-16T14:34:42.967574Z","shell.execute_reply":"2025-12-16T14:34:45.585100Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training VGG model\nvgg_model, vgg_model_history = train_model(\n    model_name = \"vgg\",\n    training_dataset = train_dataset_vgg,\n    num_epochs = config.get(\"N_EPOCHS\", 100),\n    validation_dataset = val_dataset_vgg,\n    callbacks = get_callbacks()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:34:45.586671Z","iopub.execute_input":"2025-12-16T14:34:45.587002Z","iopub.status.idle":"2025-12-16T14:36:18.194849Z","shell.execute_reply.started":"2025-12-16T14:34:45.586959Z","shell.execute_reply":"2025-12-16T14:36:18.194126Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Training ResNet Model**\n\nWe begin by training the `ResNet-Style` model on a subset of the data to quickly evaluate its performance before scaling to the full dataset.\n\n* **Prepare training and validation datasets** using a limited number of samples to ensure the model receives a representative portion of the data.\n* Train the model using the **defined pipeline**, applying callbacks to monitor progress, prevent overfitting, and adjust the learning rate automatically.\n* Observe metrics such as **accuracy, loss, precision, recall, and AUC** on both training and validation sets to assess learning performance.\n\nThis step helps us determine whether the `ResNet` architecture achieves better validation performance on the smaller subset of images and guides the decision on which model architecture is suitable for training on the full dataset.\n","metadata":{}},{"cell_type":"code","source":"# Creating training and validation datasets for ResNet model\ntrain_dataset_resnet, val_dataset_resnet = prepare_train_val_datasets(\n    X = X[:config.get(\"N_SAMPLES_TO_USE\", 8000)],\n    y = y[:config.get(\"N_SAMPLES_TO_USE\", 8000)],\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:36:18.195840Z","iopub.execute_input":"2025-12-16T14:36:18.196107Z","iopub.status.idle":"2025-12-16T14:36:18.261482Z","shell.execute_reply.started":"2025-12-16T14:36:18.196078Z","shell.execute_reply":"2025-12-16T14:36:18.260683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training ResNet model\nresnet_model, resnet_model_history = train_model(\n    model_name = \"resnet\",\n    training_dataset = train_dataset_resnet,\n    num_epochs = config.get(\"N_EPOCHS\", 100),\n    validation_dataset = val_dataset_resnet,\n    callbacks = get_callbacks()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:36:18.262384Z","iopub.execute_input":"2025-12-16T14:36:18.262684Z","iopub.status.idle":"2025-12-16T14:38:54.076511Z","shell.execute_reply.started":"2025-12-16T14:36:18.262655Z","shell.execute_reply":"2025-12-16T14:38:54.075657Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Making Predictions**\n\nWith the trained model, we can now generate predictions on new images. This involves converting the model’s output probabilities into the corresponding class labels, allowing us to identify whether an image contains a dog or a cat.\n","metadata":{}},{"cell_type":"code","source":"# VGG Model\nvgg_probabilities = vgg_model.predict(val_dataset_vgg)\nvgg_predicted_labels = (vgg_probabilities >= 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:38:58.545394Z","iopub.execute_input":"2025-12-16T14:38:58.546263Z","iopub.status.idle":"2025-12-16T14:39:00.008502Z","shell.execute_reply.started":"2025-12-16T14:38:58.546234Z","shell.execute_reply":"2025-12-16T14:39:00.007731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ResNet Model\nresnet_probabilities = resnet_model.predict(val_dataset_resnet)\nresnet_predicted_labels = (resnet_probabilities >= 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:00.403413Z","iopub.execute_input":"2025-12-16T14:39:00.404112Z","iopub.status.idle":"2025-12-16T14:39:02.546988Z","shell.execute_reply.started":"2025-12-16T14:39:00.404053Z","shell.execute_reply":"2025-12-16T14:39:02.546258Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Unbatching Validation Data for Evaluation**\n\nBefore evaluating the model on individual images, we need to convert the batched validation datasets into separate samples. This step allows us to access each image and its corresponding label individually, making it easier to compute metrics, visualize predictions, or perform detailed error analysis. By unbatching, we can directly compare model outputs with true labels for both the VGG and ResNet validation sets.","metadata":{}},{"cell_type":"code","source":"# Function to unbatch the data and extract images and labels from it\ndef extract_images_and_labels(dataset):\n    \"\"\"\n    Unbatches a TensorFlow dataset and extracts images and labels into lists.\n\n    Parameters:\n    - dataset: A batched TensorFlow dataset containing image and label pairs.\n\n    Returns:\n    - Tuple containing:\n        - images: List of image arrays (NumPy).\n        - labels: List of label arrays/values (NumPy).\n    \"\"\"\n    images = []\n    labels = []\n    \n    # Unbatching the dataset and creating an iterator to loop through it\n    iterator = dataset.unbatch().as_numpy_iterator()\n\n    for image, label in iterator:\n        images.append(image)\n        labels.append(label)\n\n    return images, labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:02.548269Z","iopub.execute_input":"2025-12-16T14:39:02.548791Z","iopub.status.idle":"2025-12-16T14:39:02.553697Z","shell.execute_reply.started":"2025-12-16T14:39:02.548769Z","shell.execute_reply":"2025-12-16T14:39:02.552628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Separating images and labels\nimages_vgg, labels_vgg = extract_images_and_labels(val_dataset_vgg)\nimages_resnet, labels_resnet = extract_images_and_labels(val_dataset_resnet)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:03.837287Z","iopub.execute_input":"2025-12-16T14:39:03.838080Z","iopub.status.idle":"2025-12-16T14:39:06.472688Z","shell.execute_reply.started":"2025-12-16T14:39:03.838029Z","shell.execute_reply":"2025-12-16T14:39:06.471760Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Visualizing Model Performance & Predictions**\n\nIn this section, we will create visualizations to better understand how the model is making predictions. By combining image with prediction data, we can see not only what the model predicts, but also how confident it is in those predictions, and whether it gets them right. The visualizations we will generate include:\n\n* **Plot training and validation metrics:** To visualize how accuracy and loss change over epochs, making it easier to spot trends like overfitting or underfitting.\n* **Display a confusion matrix:** To see how predictions are distributed across classes and identify where the model tends to make mistakes.\n* **Image with predicted and true label:** For a selected image, we will display the predicted label, the true label, and the model's confidence in its prediction.\n* **Grid of multiple predictions:** To get a broader view, we will display a grid of multiple images with their predicted labels and top prediction confidence scores, along with their top-n predictions.\n\nThese visualizations will help us assess the model's behavior on individual samples, identify common misclassifications, and gain insights into how confident the model is in differentiating between dog and cats.","metadata":{}},{"cell_type":"code","source":"# Function to visualise training and validation performance of the trained model\ndef plot_training_history(history, figsize = (12, 5)):\n    \"\"\"\n    Plots training and validation accuracy and loss.\n\n    Parameters:\n    - history: The .history dictionary from a trained Keras model.\n    - figsize: Tuple defining the figure size (width, height).\n    \"\"\"\n    \n    # Getting training history\n    hist = history.history if hasattr(history, \"history\") else history   \n\n    # Getting training and validation accuracy values\n    acc = hist.get(\"accuracy\")\n    val_acc = hist.get(\"val_accuracy\")\n\n    # Getting training and validation loss values\n    loss = hist.get(\"loss\", [])\n    val_loss = hist.get(\"val_loss\", [])\n\n    # No of epochs for Xticks\n    epochs = range(1, len(acc) + 1)\n\n    # Setting figure size\n    plt.figure(figsize=figsize, dpi=350)\n\n    # For Training and Validation Accuracy\n    plt.subplot(1, 2, 1)\n    plt.plot(epochs, acc, label=\"Training Acc\", color=\"orange\", marker='o', markersize=4)\n    plt.plot(epochs, val_acc, label=\"Validation Acc\", color=\"blue\", marker='o', markersize=4)\n    plt.title(\"Training & Validation Accuracy\", fontweight=\"bold\")\n    plt.xlabel(\"Epochs\", fontweight=\"bold\")\n    plt.ylabel(\"Accuracy\", fontweight=\"bold\")\n    plt.legend()\n    \n    # For Training and Validation Loss\n    plt.subplot(1, 2, 2)\n    plt.plot(epochs, loss, label=\"Training Loss\", color=\"orange\", marker=\"o\", markersize=4)\n    plt.plot(epochs, val_loss, label=\"Validation Loss\", color=\"blue\", marker=\"o\", markersize=4)\n    plt.title(\"Training & Validation Loss\", fontweight=\"bold\")\n    plt.xlabel(\"Epochs\", fontweight=\"bold\")\n    plt.ylabel(\"Loss\", fontweight=\"bold\")\n    plt.legend()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:06.473921Z","iopub.execute_input":"2025-12-16T14:39:06.474215Z","iopub.status.idle":"2025-12-16T14:39:06.482176Z","shell.execute_reply.started":"2025-12-16T14:39:06.474195Z","shell.execute_reply":"2025-12-16T14:39:06.481235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to plot confusion matrix of a trained model\ndef show_confusion_matrix(y_true, y_pred, class_names = config.get(\"CLASS_NAMES\", []), figsize = (12, 5)):\n    \"\"\"\n    Plots the confusion matrix.\n\n    Parameters:\n    - y_true: List of true class labels.\n    - y_pred: List of predicted class labels.\n    - class_names: List of custom class names for axis labels.\n    - figsize: Tuple defining the figure size (width, height).\n\n    Returns: None\n    \"\"\"\n    \n    # Setting the figure size\n    plt.figure(figsize=figsize, dpi = 350)\n    \n    # Computing the confusion matrix\n    cm = confusion_matrix(y_true, y_pred)\n    \n    # Plotting the confusion matrix\n    sns.heatmap(\n        cm, \n        annot=True, \n        fmt=\"d\", \n        cmap=\"Blues\", \n        cbar=False, \n        xticklabels=class_names, \n        yticklabels=class_names    \n    )\n\n    # Adding title and labels to the plot\n    plt.title(\"Confusion Matrix\", fontsize=14, pad=15, fontweight=\"bold\")\n    plt.xlabel(\"Predicted Label\", fontsize=11, fontweight=\"bold\")\n    plt.ylabel(\"True Label\", fontsize=11, fontweight=\"bold\")\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:09.133929Z","iopub.execute_input":"2025-12-16T14:39:09.134512Z","iopub.status.idle":"2025-12-16T14:39:09.140538Z","shell.execute_reply.started":"2025-12-16T14:39:09.134486Z","shell.execute_reply":"2025-12-16T14:39:09.139657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to plot an image with its predicted and true labels along with confidence\ndef plot_image_with_prediction(prediction_probs, images, true_labels, class_names=config.get(\"CLASS_NAMES\", []), sample_index=7, threshold=0.5):\n    \"\"\"\n    Plot a single image with its predicted and true label for a binary classifier.\n\n    Parameters:\n    - prediction_probs: Array of prediction probabilities for each sample.\n    - images: Array of images corresponding to the predictions.\n    - true_labels: Array of true labels (0 or 1).\n    - class_names: List of class names.\n    - sample_index: Index of the sample to visualize.\n    - threshold: Probability threshold for determining predicted class.\n\n    Returns: None.\n    \"\"\"\n    \n    # Validating sample index\n    if sample_index >= len(images):\n        raise ValueError(f\"Sample index {sample_index} is out of bounds for image list of size {len(images)}.\")\n\n    # Extracting probability and image\n    prob = prediction_probs[sample_index][0]\n    img = images[sample_index]\n\n    # Converting true label to integer\n    true_label_idx = int(true_labels[sample_index])\n\n    # Validating true label index\n    if true_label_idx >= len(class_names):\n        raise ValueError(f\"Label index {true_label_idx} is out of bounds for class_names: {class_names}\")\n\n    # Determining predicted class and confidence\n    if prob >= threshold:\n        predicted_idx = 1\n        confidence_score = prob * 100\n    else:\n        predicted_idx = 0\n        confidence_score = (1 - prob) * 100\n\n    # Normalizing image if needed\n    if img.max() > 1.0:\n        img = img / 255.0\n\n    # Setting title color\n    color = \"green\" if predicted_idx == true_label_idx else \"red\"\n\n    # Plotting the image with labels\n    plt.imshow(img)\n    plt.axis(\"off\")\n\n    # Setting the plot title\n    plt.title(\n        f\"Pred: {class_names[predicted_idx].title()} \\nTrue: {class_names[true_label_idx].title()} \\nConfidence: {confidence_score:.1f}%\",\n        color=color,\n        fontsize=12\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:10.635540Z","iopub.execute_input":"2025-12-16T14:39:10.636151Z","iopub.status.idle":"2025-12-16T14:39:10.643027Z","shell.execute_reply.started":"2025-12-16T14:39:10.636127Z","shell.execute_reply":"2025-12-16T14:39:10.642130Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to display a grid of images with predicted labels and confidence scores\ndef display_prediction_grid(prediction_probs, images, true_labels, class_names=config.get(\"CLASS_NAMES\", []), start_index=0, num_rows=3, num_cols=2, threshold=0.5):\n    \"\"\"\n    Display a grid of images with predicted and true labels with confidence.\n\n    Parameters:\n    - prediction_probs: Array of prediction probabilities for each sample.\n    - images: Array of images to display.\n    - true_labels: Array of true labels.\n    - class_names: List of class names.\n    - start_index: Index to start displaying from.\n    - num_rows: Number of rows in the grid.\n    - num_cols: Number of columns in the grid.\n    - threshold: Probability threshold for determining predicted class.\n\n    Returns: None.\n    \"\"\"\n\n    # Calculating total number of images to display\n    num_images = num_rows * num_cols\n\n    # Setting figure size based on grid dimensions\n    plt.figure(figsize=(num_cols * 4, num_rows * 4))\n\n    # Looping through each grid slot\n    for i in range(num_images):\n        current_index = start_index + i\n\n        # Stop if we run out of images\n        if current_index >= len(images):\n            break\n\n        # Creating subplot for current image\n        plt.subplot(num_rows, num_cols, i + 1)\n\n        try:\n            # Plotting the image using the helper function\n            plot_image_with_prediction(\n                prediction_probs=prediction_probs,\n                images=images,\n                true_labels=true_labels,\n                class_names=class_names,\n                sample_index=current_index,\n                threshold=threshold\n            )\n\n        except ValueError as e:\n            # Displaying error inside the grid cell\n            plt.text(0.5, 0.5, f\"Error:\\n{str(e)}\", ha=\"center\", va=\"center\")\n            plt.axis(\"off\")\n\n    # Adjusting layout to avoid overlap\n    plt.tight_layout()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:11.915351Z","iopub.execute_input":"2025-12-16T14:39:11.915646Z","iopub.status.idle":"2025-12-16T14:39:11.922793Z","shell.execute_reply.started":"2025-12-16T14:39:11.915625Z","shell.execute_reply":"2025-12-16T14:39:11.921936Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Evaluating VGG-Style Model Performance & Predictions**\n\nNow that the VGG-style model has been trained, we will take a closer look at how it performed during training and how well it predicts individual images. This step helps us move beyond raw accuracy numbers and visually understand the model’s strengths and weaknesses.\n\n* **Plot training history**\n* **Generate a confusion matrix**\n* **Display a prediction grid**\n\nThese visual checks make it easier to interpret the model’s behavior, identify common errors, and better understand how reliably it distinguishes between classes.\n","metadata":{}},{"cell_type":"code","source":"# Visualising model performance of the trained VGG model\nplot_training_history(vgg_model_history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:14.038375Z","iopub.execute_input":"2025-12-16T14:39:14.039160Z","iopub.status.idle":"2025-12-16T14:39:14.858099Z","shell.execute_reply.started":"2025-12-16T14:39:14.039135Z","shell.execute_reply":"2025-12-16T14:39:14.857164Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualising confusion matrix of the trained VGG model\nshow_confusion_matrix(\n    y_true = labels_vgg,\n    y_pred = vgg_predicted_labels\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:20.719392Z","iopub.execute_input":"2025-12-16T14:39:20.719973Z","iopub.status.idle":"2025-12-16T14:39:21.126876Z","shell.execute_reply.started":"2025-12-16T14:39:20.719945Z","shell.execute_reply":"2025-12-16T14:39:21.125967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Displaying predictions from a VGG-Style model\ndisplay_prediction_grid(\n    prediction_probs=vgg_probabilities,\n    images=images_vgg,\n    true_labels=labels_vgg,\n    start_index = 0,\n    num_rows = 5,\n    num_cols = 5\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:24.713127Z","iopub.execute_input":"2025-12-16T14:39:24.713419Z","iopub.status.idle":"2025-12-16T14:39:29.546016Z","shell.execute_reply.started":"2025-12-16T14:39:24.713400Z","shell.execute_reply":"2025-12-16T14:39:29.544499Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"### **Evaluating ResNet-Style Model Performance & Predictions**\n\nNow, we will examine how well our model performs on training and on individual validation images. These visualizations help us understand not just overall accuracy, but also where the model succeeds and where it might make mistakes.\n\n* **Plot training history**\n* **Generate a confusion matrix**\n* **Display a prediction grid**\n\nThis step lets us confirm that the ResNet-style model generalizes effectively, highlights its improvements over simpler models, and provides insights into its real-world predictive behavior.\n","metadata":{}},{"cell_type":"code","source":"# Visualising model performance of the trained ResNet model\nplot_training_history(resnet_model_history)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:30.455573Z","iopub.execute_input":"2025-12-16T14:39:30.455868Z","iopub.status.idle":"2025-12-16T14:39:31.270000Z","shell.execute_reply.started":"2025-12-16T14:39:30.455847Z","shell.execute_reply":"2025-12-16T14:39:31.269146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualising confusion matrix of the trained ResNet model\nshow_confusion_matrix(\n    y_true = labels_resnet,\n    y_pred = resnet_predicted_labels\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:40.671560Z","iopub.execute_input":"2025-12-16T14:39:40.671903Z","iopub.status.idle":"2025-12-16T14:39:41.098192Z","shell.execute_reply.started":"2025-12-16T14:39:40.671878Z","shell.execute_reply":"2025-12-16T14:39:41.097308Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Displaying predictions from a ResNet-Style model\ndisplay_prediction_grid(\n    prediction_probs=resnet_probabilities,\n    images=images_resnet,\n    true_labels=labels_resnet,\n    start_index = 0,\n    num_rows = 5,\n    num_cols = 5\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:41.099599Z","iopub.execute_input":"2025-12-16T14:39:41.099830Z","iopub.status.idle":"2025-12-16T14:39:45.350770Z","shell.execute_reply.started":"2025-12-16T14:39:41.099812Z","shell.execute_reply":"2025-12-16T14:39:45.349366Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Saving and Loading a Trained Model**\n\nAfter training and evaluating our model, it’s important to save it so we can reuse it later without retraining. Saving the model preserves both its architecture and trained weights, making it easy to continue training, run predictions, or share with others.\n\n* **Saving the model:** We will save the model with a filename that includes the current timestamp and an optional suffix. This helps keep multiple versions organized and easy to identify.\n* **Loading the model:** We can load a previously saved model to restore it to its exact trained state, allowing us to continue training or make predictions seamlessly.\n\nBy saving and loading models in this way, we make the training process efficient, reproducible, and ready for further experimentation.\n","metadata":{}},{"cell_type":"code","source":"# Function to save the trained model\ndef save_model_with_timestamp(model, suffix=\"\", base_dir=config.get(\"SAVE_MODEL_TO\", \"\")):\n    \"\"\"\n    Save a trained Keras model with a timestamped filename.\n\n    Parameters:\n    - model: Trained TensorFlow/Keras model.\n    - suffix: Optional text appended to the filename.\n    - base_dir: Directory where the model will be saved. Created if missing.\n\n    Returns:\n    - str: Full path of the saved model file.\n    \"\"\"\n\n    # Using default directory if base directory is none\n    if base_dir == \"\":\n        base_dir = \"./saved-models/\"\n\n    # Creating base directory if it doesn't exist\n    os.makedirs(base_dir, exist_ok=True)\n\n    # Creating filename with current timestamp\n    timestamp = datetime.datetime.now().strftime(\"%Y%m%d%H%M%S\")\n    filename = f\"model_{timestamp}\"\n\n    # Appending suffix if provided\n    if suffix:\n        filename += f\"_{suffix}\"\n\n    filename += \".keras\"\n\n    # Createing full model path\n    model_filepath = os.path.join(base_dir, filename)\n\n    # Saving the model\n    print(f\"Saving model to: {model_filepath}...\")\n    try:\n        \n        model.save(model_filepath)\n        print(\"Model saved successfully\")\n        return model_filepath\n    except Exception as e:\n        print(f\"Failed to save model: {e}\")\n        raise e\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:45.986461Z","iopub.execute_input":"2025-12-16T14:39:45.986751Z","iopub.status.idle":"2025-12-16T14:39:45.992932Z","shell.execute_reply.started":"2025-12-16T14:39:45.986731Z","shell.execute_reply":"2025-12-16T14:39:45.992230Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to load a saved Keras model\ndef load_model_from_filepath(model_filepath, mode=True):\n    \"\"\"\n    Load a trained Keras model from a file path.\n\n    Parameters:\n    - model_filepath: Full path to the saved .keras or .h5 file.\n    - safe_mode: If True, prevents loading models with potentially unsafe Lambda layers.\n\n    Returns:\n    - tf.keras.Model: Loaded Keras model.\n    \"\"\"\n    # Check if the saved model file exists\n    if not os.path.exists(model_filepath):\n        raise FileNotFoundError(f\"No model found at {model_filepath}\")\n\n    print(f\"Loading saved model from: {model_filepath}...\")\n\n    try:\n        \n        # Loading the Keras model\n        model = tf.keras.models.load_model(model_filepath, safe_mode=mode)\n        print(\"Model loaded successfully\")\n        return model\n    \n    except Exception as e:\n    \n        # Handling loading errors\n        print(f\"Failed to load model. Reason: {e}\")\n        raise e\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:48.911478Z","iopub.execute_input":"2025-12-16T14:39:48.912039Z","iopub.status.idle":"2025-12-16T14:39:48.917495Z","shell.execute_reply.started":"2025-12-16T14:39:48.912012Z","shell.execute_reply":"2025-12-16T14:39:48.916630Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Model Training With Full Dataset**\n\nAfter testing different architectures, we saw that the `VGG-Style` model performed slightly better than the `ResNet-Style` model on a subset of **8,000 images**. Since the difference was small and both models are solid choices, we will move forward with `VGG-Style` and train it on the full dataset to further improve its performance and prepare it for evaluation.\n\n* **Prepare full datasets:** Create training and validation datasets from all available images and labels.\n* **Train the VGG model:** Run training for multiple epochs with callbacks to monitor progress and improve learning.\n* **Make predictions:** Generate predictions on the validation set to assess the model’s performance.\n* **Unbatch validation data:** Separate the validation dataset into individual images and labels for easier analysis.\n* **Visualize predictions:** Display a grid of predicted images along with their true labels to inspect performance visually.\n* **Save and load model:** Save the fully trained model with a timestamped filename, then load it back to verify reproducibility.\n* **Confirm loaded model predictions:** Run predictions with the loaded model to ensure consistency with the original model.\n\nThis step allows us to fully train, evaluate, and preserve the VGG model, ensuring it’s accurate, reliable, and ready for further experimentation.","metadata":{}},{"cell_type":"code","source":"# Creating training and validation datasets for VGG-Style model\ntrain_dataset_full, val_dataset_full = prepare_train_val_datasets(\n    X = X,\n    y = y,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:39:52.647104Z","iopub.execute_input":"2025-12-16T14:39:52.647563Z","iopub.status.idle":"2025-12-16T14:39:52.718374Z","shell.execute_reply.started":"2025-12-16T14:39:52.647532Z","shell.execute_reply":"2025-12-16T14:39:52.717599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Training VGG model on full dataset\nmodel, model_history = train_model(\n    model_name = \"vgg\",\n    training_dataset = train_dataset_full,\n    num_epochs = config.get(\"N_EPOCHS\", 100),\n    validation_dataset = val_dataset_full,\n    callbacks = get_callbacks()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-16T14:40:00.750289Z","iopub.execute_input":"2025-12-16T14:40:00.751033Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Function to make predictions on a dataset and visualize them in a grid\ndef predict_and_visualize_model(model, dataset, class_names=config.get(\"CLASS_NAMES\", []), num_rows=3, num_cols=2):\n    \"\"\"\n    Run predictions on a dataset and displays results in a grid.\n\n    Parameters:\n    - model: Trained Keras model.\n    - dataset: tf.data.Dataset to predict on.\n    - class_names: List of class names.\n    - num_rows: Number of rows in the grid.\n    - num_cols: Number of columns in the grid.\n\n    Returns:\n    - prediction_probs: Model's prediction probabilities.\n    - images: Images from the dataset.\n    - true_labels: True labels corresponding to images.\n    \"\"\"\n    \n    print(\"Making predictions...\")\n    \n    # Making predictions\n    prediction_probs = model.predict(dataset)\n\n    print(\"Extracting images and labels...\")\n\n    # Extracting images and true labels from dataset\n    images, true_labels = extract_images_and_labels(dataset)\n\n    # Validating number of samples\n    num_samples = num_rows * num_cols\n    if len(images) < num_samples:\n        raise ValueError(f\"Requested {num_samples} samples, but dataset only has {len(images)}.\")\n\n    print(\"Generating visualization grid...\")\n    \n    # Displaying the prediction grid\n    display_prediction_grid(\n        prediction_probs=prediction_probs,\n        images=images,\n        true_labels=true_labels,\n        class_names=class_names,\n        num_rows=num_rows,\n        num_cols=num_cols,\n        start_index=0\n    )\n\n    return prediction_probs, images, true_labels","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Predicting and visualizing them \npreds, images, labels = predict_and_visualize_model(\n    model=model,\n    dataset=val_dataset_full,\n    class_names=config.get(\"CLASS_NAMES\", []),\n    num_rows=5,\n    num_cols=5\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualising model performance of the trained model\nplot_training_history(model_history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Converting probabilities to binary class labels\npred_labels = (preds >= 0.5).astype(int)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualising confusion matrix of the trained model\nshow_confusion_matrix(\n    y_true = labels,\n    y_pred = pred_labels\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Saving the fully trained model\nsuffix = \"VggCNN_CatsVsDog_e100_b32\"\nsaved_model_filepath = save_model_with_timestamp(model, suffix = suffix)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Loading saved model\nloaded_model = load_model_from_filepath(saved_model_filepath, mode = True)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Using the loaded model to make predictions and visualizing the results\npreds, images, labels = predict_and_visualize_model(\n    model=loaded_model,\n    dataset=val_dataset_full,\n    class_names=config.get(\"CLASS_NAMES\", []),\n    num_rows=5,\n    num_cols=5\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---","metadata":{}},{"cell_type":"markdown","source":"## **Making Predictions on Test Data**\n\nAfter training and validating the model, we are ready to generate predictions on the unseen test images. This step helps us evaluate how well the model generalizes and prepares a structured submission for further analysis or competition entry.\n\n* **Prepare test data:** Extract image IDs from filenames to identify each image in the submission.\n* **Build the test dataset:** Load all test images in a consistent format without shuffling to match the preprocessing used during training.\n* **Generate predictions:** Use the trained model to predict probabilities for each test image.\n* **Convert probabilities to class labels:** Apply a threshold (e.g., 0.5) to determine the final predicted class for each image.\n* **Save predictions to CSV:** Create a submission file containing image IDs and predicted labels for easy sharing and analysis.\n\nThis step ensures that we have an organized set of predictions, giving a clear view of the model’s performance on completely unseen data and producing a ready-to-use submission file.","metadata":{}},{"cell_type":"code","source":"def generate_submission_file(model, test_df, output_filename=\"submission.csv\", threshold=0.5):\n    \"\"\"\n    Generate a submission file using model predictions on test images.\n\n    Parameters:\n    - model: Trained model used for generating predictions.\n    - test_df: DataFrame containing test image paths.\n    - output_filename: Name of the output CSV file.\n    - threshold: Probability threshold for determining predicted class.\n\n    Returns: None.\n    \"\"\"\n\n    print(\"Preparing test data...\")\n\n    # Getting image IDs from file names\n    test_df[\"id\"] = test_df[\"image_path\"].apply(lambda x: int(os.path.basename(x).split(\".\")[0]))\n\n    # Building test dataset\n    print(f\"Processing {len(test_df)} images...\")\n    test_dataset = create_dataset(\n        X=test_df[\"image_path\"],\n        dataset_type=\"test\",\n        shuffle=False\n    )\n\n    print(\"Predicting...\")\n    raw_predictions = model.predict(test_dataset, verbose=1)\n\n    # Flattening predictions if needed\n    predictions = raw_predictions.flatten() if raw_predictions.ndim > 1 else raw_predictions\n\n    # Creating submission DataFrame\n    submission_df = pd.DataFrame({\n        \"id\": test_df[\"id\"],\n        \"preds\": predictions\n    })\n\n    # Converting predicted probabilities to binary class labels\n    submission_df[\"label\"] = submission_df[\"preds\"].apply(lambda x: 1 if x >= threshold else 0)\n\n    # Saving submission file\n    submission_df.drop(\"preds\", axis = 1).to_csv(output_filename, index=False)\n    print(f\"Submission saved to '{output_filename}'\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Generating submission file\ngenerate_submission_file(\n    model = loaded_model,\n    test_df = test_df\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}