{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":3362,"databundleVersionId":31148}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Dogs vs Cats image classification","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","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:04.939083Z","iopub.execute_input":"2026-05-05T07:40:04.939381Z","iopub.status.idle":"2026-05-05T07:40:05.813917Z","shell.execute_reply.started":"2026-05-05T07:40:04.939358Z","shell.execute_reply":"2026-05-05T07:40:05.813213Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\npath = kagglehub.competition_download('dogs-vs-cats')\n\nprint(\"Path to competition files:\", path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:05.815732Z","iopub.execute_input":"2026-05-05T07:40:05.816141Z","iopub.status.idle":"2026-05-05T07:40:07.042020Z","shell.execute_reply.started":"2026-05-05T07:40:05.816117Z","shell.execute_reply":"2026-05-05T07:40:07.041369Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Unzip data:","metadata":{}},{"cell_type":"code","source":"train = '/kaggle/input/competitions/dogs-vs-cats/train.zip'\ntest  = '/kaggle/input/competitions/dogs-vs-cats/test1.zip'\n\nos.makedirs('/kaggle/working/dogs-vs-cats', exist_ok=True)\n\nimport zipfile\nwith zipfile.ZipFile(train, 'r') as z:\n    z.extractall('/kaggle/working/dogs-vs-cats')\n\nwith zipfile.ZipFile(test, 'r') as z:\n    z.extractall('/kaggle/working/dogs-vs-cats')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:07.042946Z","iopub.execute_input":"2026-05-05T07:40:07.043334Z","iopub.status.idle":"2026-05-05T07:40:20.447784Z","shell.execute_reply.started":"2026-05-05T07:40:07.043309Z","shell.execute_reply":"2026-05-05T07:40:20.447108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#important libraries\n\nimport os\nimport random\nimport zipfile\nimport warnings\nfrom collections import Counter\n\nimport cv2\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nwarnings.filterwarnings(\"ignore\")\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:20.448651Z","iopub.execute_input":"2026-05-05T07:40:20.448885Z","iopub.status.idle":"2026-05-05T07:40:20.722940Z","shell.execute_reply.started":"2026-05-05T07:40:20.448863Z","shell.execute_reply":"2026-05-05T07:40:20.721479Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"===========================================","metadata":{}},{"cell_type":"markdown","source":"# EDA","metadata":{}},{"cell_type":"code","source":"data_dir = '/kaggle/working/dogs-vs-cats'\nfolders = os.listdir(data_dir)\nimage_counts = {fld: len(os.listdir(os.path.join(data_dir, fld))) for fld in folders}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:20.725460Z","iopub.execute_input":"2026-05-05T07:40:20.725699Z","iopub.status.idle":"2026-05-05T07:40:20.747694Z","shell.execute_reply.started":"2026-05-05T07:40:20.725677Z","shell.execute_reply":"2026-05-05T07:40:20.747137Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.bar(image_counts.keys(), image_counts.values())\nplt.title(\"Image Count per Folder\")\nplt.xlabel(\"Folder\")\nplt.ylabel(\"Count\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:20.748524Z","iopub.execute_input":"2026-05-05T07:40:20.748782Z","iopub.status.idle":"2026-05-05T07:40:20.884780Z","shell.execute_reply.started":"2026-05-05T07:40:20.748753Z","shell.execute_reply":"2026-05-05T07:40:20.884195Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The dataset contains two main folders: one for training images and one for test images. The training folder has more images than the test folder, which is expected because the model needs labeled training data to learn from.","metadata":{}},{"cell_type":"code","source":"dimensions = []\nfor fld in folders:\n    folder = os.path.join(data_dir, fld)\n    for img_name in os.listdir(folder):\n        img_path = os.path.join(folder, img_name)\n        with Image.open(img_path) as img:\n            dimensions.append(img.size)\n\nwidths, heights = zip(*dimensions)\nplt.hist(widths, bins=20, alpha=0.5, label='Width')\nplt.hist(heights, bins=20, alpha=0.5, label='Height')\nplt.legend()\nplt.title(\"Image Dimensions Distribution\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:20.885689Z","iopub.execute_input":"2026-05-05T07:40:20.886140Z","iopub.status.idle":"2026-05-05T07:40:23.522523Z","shell.execute_reply.started":"2026-05-05T07:40:20.886117Z","shell.execute_reply":"2026-05-05T07:40:23.521755Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The images in the dataset do not all have the same original width and height. This means the images need to be resized before being passed into the neural network, since CNN models require a consistent input shape.","metadata":{}},{"cell_type":"code","source":"formats = []\nmodes = []\n\nfor fld in folders:\n    folder = os.path.join(data_dir, fld)\n    for img_name in os.listdir(folder):\n        img_path = os.path.join(folder, img_name)\n        with Image.open(img_path) as img:\n            formats.append(img.format)\n            modes.append(img.mode)\n\nprint(Counter(formats))\nprint(Counter(modes))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:23.523522Z","iopub.execute_input":"2026-05-05T07:40:23.523859Z","iopub.status.idle":"2026-05-05T07:40:25.753485Z","shell.execute_reply.started":"2026-05-05T07:40:23.523834Z","shell.execute_reply":"2026-05-05T07:40:25.752610Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"All images in the dataset are in JPEG format and use RGB color mode. This is useful because the data is consistent in terms of file type and color channels, so no extra conversion is needed before training.\nSince the images are RGB, the model input should use 3 color channels.","metadata":{}},{"cell_type":"code","source":"import random\n\nsamples_per_fld = 16\n\nfor fld in folders:\n    folder = os.path.join(data_dir, fld)\n    images = random.sample(os.listdir(folder), samples_per_fld)\n\n    plt.figure(figsize=(8, 8))\n    plt.suptitle(f\"Folder: {fld}\", fontsize=16)\n\n    for i, img_name in enumerate(images):\n        img_path = os.path.join(folder, img_name)\n        img = Image.open(img_path).convert(\"RGB\")\n\n        plt.subplot(4, 4, i + 1)\n        plt.imshow(img)\n        plt.axis('off')\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:25.754551Z","iopub.execute_input":"2026-05-05T07:40:25.754961Z","iopub.status.idle":"2026-05-05T07:40:26.916575Z","shell.execute_reply.started":"2026-05-05T07:40:25.754915Z","shell.execute_reply":"2026-05-05T07:40:26.915651Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The sample images show that the dataset contains a lot of variation.","metadata":{}},{"cell_type":"markdown","source":"===========================================","metadata":{}},{"cell_type":"code","source":"filenames = os.listdir('/kaggle/working/dogs-vs-cats/train')\nlabels    = [f.split(\".\")[0] for f in filenames]\n\ndata = pd.DataFrame({\"filename\": filenames, \"label\": labels})\n\nprint(f\"\\nTotal : {len(data):,}\")\nprint(data['label'].value_counts().to_string())\ndata.head(8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:26.920219Z","iopub.execute_input":"2026-05-05T07:40:26.920471Z","iopub.status.idle":"2026-05-05T07:40:26.976982Z","shell.execute_reply.started":"2026-05-05T07:40:26.920451Z","shell.execute_reply":"2026-05-05T07:40:26.976415Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The labels were extracted directly from the image filenames. Since each filename starts with either `cat` or `dog`, this makes it easy to create the target label column automatically.","metadata":{}},{"cell_type":"code","source":"image_counts = data['label'].value_counts()\n\nplt.bar(image_counts.index, image_counts.values)\nplt.title(\"Image Count per Class\")\nplt.xlabel(\"Class\")\nplt.ylabel(\"Count\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:26.977871Z","iopub.execute_input":"2026-05-05T07:40:26.978228Z","iopub.status.idle":"2026-05-05T07:40:27.073966Z","shell.execute_reply.started":"2026-05-05T07:40:26.978199Z","shell.execute_reply":"2026-05-05T07:40:27.073404Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The training dataset is perfectly balanced, with 12,500 cat images and 12,500 dog images, which is useful because the model will not be biased toward one class more than the other.","metadata":{}},{"cell_type":"code","source":"test_filenames = os.listdir('/kaggle/working/dogs-vs-cats/test1')\n\ntest_data = pd.DataFrame({\n    \"filename\": test_filenames\n})\n\nprint(f\"\\nTotal test images: {len(test_data):,}\")\ntest_data.head(8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:40:27.074830Z","iopub.execute_input":"2026-05-05T07:40:27.075100Z","iopub.status.idle":"2026-05-05T07:40:27.091353Z","shell.execute_reply.started":"2026-05-05T07:40:27.075069Z","shell.execute_reply":"2026-05-05T07:40:27.090717Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data preparation","metadata":{}},{"cell_type":"markdown","source":"We split the labeled training data into 80% training and 20% validation. This keeps most of the images available for training while still leaving a large enough validation set. We did not create a separate test split from the training data because the Kaggle competition already provides a separate test folder. Since those test images do not include labels, I use the validation set for model evaluation during development and keep the Kaggle test set for final predictions.","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nX_train, X_val = train_test_split(\n    data, test_size=0.2, stratify=data['label'], random_state=42\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:41:43.211679Z","iopub.execute_input":"2026-05-05T07:41:43.212245Z","iopub.status.idle":"2026-05-05T07:41:43.989588Z","shell.execute_reply.started":"2026-05-05T07:41:43.212198Z","shell.execute_reply":"2026-05-05T07:41:43.988960Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Once separated into two dataframes, we can check if the number of dogs and cats is correctly distributed","metadata":{}},{"cell_type":"code","source":"print(\"Train counts:\")\nprint(X_train['label'].value_counts())\n\nprint(\"\\nValidation counts:\")\nprint(X_val['label'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:41:47.636766Z","iopub.execute_input":"2026-05-05T07:41:47.637649Z","iopub.status.idle":"2026-05-05T07:41:47.648140Z","shell.execute_reply.started":"2026-05-05T07:41:47.637619Z","shell.execute_reply":"2026-05-05T07:41:47.647024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, ax = plt.subplots(1, 2, figsize=(8, 4))\n\nX_train['label'].value_counts().plot(\n    kind='bar', ax=ax[0], title='Train'\n)\nX_val['label'].value_counts().plot(\n    kind='bar', ax=ax[1], title='Validation'\n)\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:41:48.806963Z","iopub.execute_input":"2026-05-05T07:41:48.807793Z","iopub.status.idle":"2026-05-05T07:41:49.141122Z","shell.execute_reply.started":"2026-05-05T07:41:48.807762Z","shell.execute_reply":"2026-05-05T07:41:49.140448Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see that the class distribution remains balanced after the split.","metadata":{}},{"cell_type":"markdown","source":"# Baseline model","metadata":{}},{"cell_type":"markdown","source":"This CNN model will be used as a baseline for other models. Its a CNN used from a previous draft of this notebook.","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (\n    Conv2D, MaxPooling2D, Dense, Flatten,\n    Dropout, BatchNormalization\n)\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nmodel = Sequential([\n    Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    Conv2D(64, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n\n    Conv2D(64, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    BatchNormalization(),\n    MaxPooling2D(pool_size=(2, 2)),\n    Dropout(0.2),\n    \n    Flatten(),\n    Dense(128, activation='relu'),\n    BatchNormalization(),\n    Dropout(0.5),\n\n    Dense(1, activation='sigmoid')\n])\n\nmodel.compile(\n    optimizer = 'adam',\n    loss      = 'binary_crossentropy',\n    metrics   = ['accuracy']\n)\n\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:11:23.556371Z","iopub.execute_input":"2026-05-04T09:11:23.557220Z","iopub.status.idle":"2026-05-04T09:11:33.794247Z","shell.execute_reply.started":"2026-05-04T09:11:23.557188Z","shell.execute_reply":"2026-05-04T09:11:33.793610Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Then we do data augmentation only on the train generator. Validation and test ones dont need it, only normalization","metadata":{}},{"cell_type":"code","source":"image_gen = ImageDataGenerator(rotation_range=30,\n                               width_shift_range=0.1,\n                               height_shift_range=0.1,\n                               rescale=1.0/255,\n                               shear_range=0.1,\n                               zoom_range=0.2,\n                               horizontal_flip=True,\n                               fill_mode='reflect'\n                              )\n\nval_gen = ImageDataGenerator(rescale=1.0/255)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:11:41.724996Z","iopub.execute_input":"2026-05-04T09:11:41.725954Z","iopub.status.idle":"2026-05-04T09:11:41.730085Z","shell.execute_reply.started":"2026-05-04T09:11:41.725914Z","shell.execute_reply":"2026-05-04T09:11:41.729197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"batch_size = 32\nimage_shape = (150, 150, 3)\n\ntrain_image_gen = image_gen.flow_from_dataframe(X_train,'/kaggle/working/dogs-vs-cats/train',\n                                               x_col='filename',\n                                               y_col='label',\n                                               target_size=image_shape[:2],\n                                               batch_size=batch_size,\n                                               class_mode='binary')\n\nval_image_gen = val_gen.flow_from_dataframe(X_val,'/kaggle/working/dogs-vs-cats/train',\n                                               x_col='filename',\n                                               y_col='label',\n                                               target_size=image_shape[:2],\n                                               batch_size=batch_size,\n                                               class_mode='binary',\n                                               shuffle = False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:11:45.715842Z","iopub.execute_input":"2026-05-04T09:11:45.716499Z","iopub.status.idle":"2026-05-04T09:11:45.927250Z","shell.execute_reply.started":"2026-05-04T09:11:45.716467Z","shell.execute_reply":"2026-05-04T09:11:45.926427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_image_gen.class_indices","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:11:48.073631Z","iopub.execute_input":"2026-05-04T09:11:48.074260Z","iopub.status.idle":"2026-05-04T09:11:48.079578Z","shell.execute_reply.started":"2026-05-04T09:11:48.074231Z","shell.execute_reply":"2026-05-04T09:11:48.078698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import ReduceLROnPlateau\nfrom tensorflow.keras.callbacks import EarlyStopping\n\nlr_reduction = ReduceLROnPlateau(\n    monitor='val_accuracy',\n    patience=2,\n    factor=0.5,\n    min_lr=1e-5,\n    verbose=1\n)\n\nearly_stop = EarlyStopping(\n    monitor='val_loss',\n    patience=3,\n    restore_best_weights=True,\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T08:58:26.645016Z","iopub.execute_input":"2026-05-05T08:58:26.645555Z","iopub.status.idle":"2026-05-05T08:58:26.652407Z","shell.execute_reply.started":"2026-05-05T08:58:26.645519Z","shell.execute_reply":"2026-05-05T08:58:26.651646Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Then we train our model, on 20 epochs to enhance accuracy, and using the the callback methods from above.","metadata":{}},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings('ignore')\n\nresults = model.fit(train_image_gen,epochs=20, validation_data=val_image_gen,\n                   callbacks=[early_stop, lr_reduction],\n                   verbose = 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:12:07.481772Z","iopub.execute_input":"2026-05-04T09:12:07.482507Z","iopub.status.idle":"2026-05-04T09:54:37.776258Z","shell.execute_reply.started":"2026-05-04T09:12:07.482478Z","shell.execute_reply":"2026-05-04T09:54:37.775568Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Simple error analysis 1","metadata":{}},{"cell_type":"markdown","source":"Once the model is trained, we do a simple error analysis","metadata":{}},{"cell_type":"code","source":"results.history['accuracy']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:54:47.980976Z","iopub.execute_input":"2026-05-04T09:54:47.981344Z","iopub.status.idle":"2026-05-04T09:54:47.986829Z","shell.execute_reply.started":"2026-05-04T09:54:47.981319Z","shell.execute_reply":"2026-05-04T09:54:47.986194Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.plot(results.history['accuracy'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:54:51.687155Z","iopub.execute_input":"2026-05-04T09:54:51.687775Z","iopub.status.idle":"2026-05-04T09:54:51.810758Z","shell.execute_reply.started":"2026-05-04T09:54:51.687747Z","shell.execute_reply":"2026-05-04T09:54:51.810110Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_loss, val_accuracy = model.evaluate(val_image_gen)\n\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:54:55.449783Z","iopub.execute_input":"2026-05-04T09:54:55.450500Z","iopub.status.idle":"2026-05-04T09:55:02.180685Z","shell.execute_reply.started":"2026-05-04T09:54:55.450468Z","shell.execute_reply":"2026-05-04T09:55:02.179805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_probs = model.predict(val_image_gen, batch_size = batch_size, verbose = 1)\n\nval_preds = (val_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:55:05.487653Z","iopub.execute_input":"2026-05-04T09:55:05.488533Z","iopub.status.idle":"2026-05-04T09:55:13.396499Z","shell.execute_reply.started":"2026-05-04T09:55:05.488492Z","shell.execute_reply":"2026-05-04T09:55:13.395449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_true = val_image_gen.labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:55:17.133127Z","iopub.execute_input":"2026-05-04T09:55:17.133956Z","iopub.status.idle":"2026-05-04T09:55:17.137669Z","shell.execute_reply.started":"2026-05-04T09:55:17.133916Z","shell.execute_reply":"2026-05-04T09:55:17.136896Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(classification_report(\n    y_true,\n    val_preds,\n    target_names=['cat', 'dog']\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:55:31.897995Z","iopub.execute_input":"2026-05-04T09:55:31.898946Z","iopub.status.idle":"2026-05-04T09:55:31.917737Z","shell.execute_reply.started":"2026-05-04T09:55:31.898911Z","shell.execute_reply":"2026-05-04T09:55:31.917101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm = confusion_matrix(y_true, val_preds)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm, annot=True, fmt='d',\n            xticklabels=['cat', 'dog'],\n            yticklabels=['cat', 'dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:55:46.762829Z","iopub.execute_input":"2026-05-04T09:55:46.763530Z","iopub.status.idle":"2026-05-04T09:55:47.179686Z","shell.execute_reply.started":"2026-05-04T09:55:46.763501Z","shell.execute_reply":"2026-05-04T09:55:47.178931Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Prediction 1","metadata":{}},{"cell_type":"markdown","source":"Since the submisson for the Cats-vs-Dogs is closed, we test with 10 random pictures from the test1 folder","metadata":{}},{"cell_type":"code","source":"img1 = cv2.imread('/kaggle/working/dogs-vs-cats/test1/7147.jpg')\nimg1 = cv2.cvtColor(img1,cv2.COLOR_BGR2RGB)\nplt.imshow(img1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:56:01.713332Z","iopub.execute_input":"2026-05-04T09:56:01.714125Z","iopub.status.idle":"2026-05-04T09:56:01.999592Z","shell.execute_reply.started":"2026-05-04T09:56:01.714087Z","shell.execute_reply":"2026-05-04T09:56:01.998834Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_sample = test_data.iloc[:10].copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:56:14.604775Z","iopub.execute_input":"2026-05-04T09:56:14.605599Z","iopub.status.idle":"2026-05-04T09:56:14.609673Z","shell.execute_reply.started":"2026-05-04T09:56:14.605560Z","shell.execute_reply":"2026-05-04T09:56:14.608811Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_sample","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:57:19.021626Z","iopub.execute_input":"2026-05-04T09:57:19.022627Z","iopub.status.idle":"2026-05-04T09:57:19.029454Z","shell.execute_reply.started":"2026-05-04T09:57:19.022596Z","shell.execute_reply":"2026-05-04T09:57:19.028798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_datagen = ImageDataGenerator(rescale=1/255)\n\nsample_gen = sample_datagen.flow_from_dataframe(\n    test_sample,\n    '/kaggle/working/dogs-vs-cats/test1',\n    x_col='filename',\n    y_col=None,\n    target_size=(150, 150),\n    batch_size=1,\n    class_mode=None,\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:45:34.083815Z","iopub.execute_input":"2026-05-05T12:45:34.084377Z","iopub.status.idle":"2026-05-05T12:45:34.091545Z","shell.execute_reply.started":"2026-05-05T12:45:34.084345Z","shell.execute_reply":"2026-05-05T12:45:34.090887Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_probs = model.predict(sample_gen)\n\nsample_preds = (sample_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:58:07.088626Z","iopub.execute_input":"2026-05-04T09:58:07.088906Z","iopub.status.idle":"2026-05-04T09:58:07.970681Z","shell.execute_reply.started":"2026-05-04T09:58:07.088886Z","shell.execute_reply":"2026-05-04T09:58:07.969996Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labels = ['cat', 'dog']\n\n\nplt.figure(figsize=(15, 6))\n\nfor i in range(10):\n    img = plt.imread(\n        f\"/kaggle/working/dogs-vs-cats/test1/{test_sample.iloc[i]['filename']}\"\n    )\n    \n    plt.subplot(2, 5, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    pred_label = labels[sample_preds[i]]\n    prob = sample_probs[i][0]\n    \n    plt.title(f\"{pred_label}\\nprob={prob:.2f}\")\n\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T09:58:16.341338Z","iopub.execute_input":"2026-05-04T09:58:16.342217Z","iopub.status.idle":"2026-05-04T09:58:17.092860Z","shell.execute_reply.started":"2026-05-04T09:58:16.342186Z","shell.execute_reply":"2026-05-04T09:58:17.091870Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can see that, from these 10 random photos, all of them appear to be correct predictions.","metadata":{}},{"cell_type":"markdown","source":"# Model 2 - CNN v2 (simpler version)","metadata":{}},{"cell_type":"markdown","source":"With this second model, we attempt to make a simpler version of the baseline. It will probably perform worse then the baseline model, but its purpose is to highlight the importance of the callbacks and parameters that we removed from the baseline model","metadata":{}},{"cell_type":"markdown","source":"Here, we remove BatchNormalization and most Dropouts, as well as the the callbacks, EarlyStopping and ReduceLROnPlateau","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import (\n    Conv2D, MaxPooling2D, Dense, Flatten,\n    Dropout, BatchNormalization\n)\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nmodel2 = Sequential([\n    Conv2D(64, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    MaxPooling2D(pool_size=(2, 2)),\n\n    Conv2D(64, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    MaxPooling2D(pool_size=(2, 2)),\n\n    Conv2D(64, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n    MaxPooling2D(pool_size=(2, 2)),\n    \n    Flatten(),\n    Dense(64, activation='relu'),\n    Dropout(0.5),\n\n    Dense(1, activation='sigmoid')\n])\n\nmodel2.compile(\n    optimizer = 'adam',\n    loss      = 'binary_crossentropy',\n    metrics   = ['accuracy']\n)\n\nmodel2.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T10:16:10.744643Z","iopub.execute_input":"2026-05-04T10:16:10.745473Z","iopub.status.idle":"2026-05-04T10:16:10.806323Z","shell.execute_reply.started":"2026-05-04T10:16:10.745444Z","shell.execute_reply":"2026-05-04T10:16:10.805753Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings('ignore')\n\nresults2 = model2.fit(train_image_gen,epochs=20, validation_data=val_image_gen,\n                   verbose = 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T10:16:21.914119Z","iopub.execute_input":"2026-05-04T10:16:21.914404Z","iopub.status.idle":"2026-05-04T10:58:26.006839Z","shell.execute_reply.started":"2026-05-04T10:16:21.914382Z","shell.execute_reply":"2026-05-04T10:58:26.006253Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"It appears that after the same number of epochs as the baseline model, we achieve a lower accuracy, which we will confirm after the simple error analysis","metadata":{}},{"cell_type":"markdown","source":"## Simple error analysis 2","metadata":{}},{"cell_type":"code","source":"results2.history['accuracy']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T10:58:57.389352Z","iopub.execute_input":"2026-05-04T10:58:57.389644Z","iopub.status.idle":"2026-05-04T10:58:57.395379Z","shell.execute_reply.started":"2026-05-04T10:58:57.389622Z","shell.execute_reply":"2026-05-04T10:58:57.394669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.plot(results2.history['accuracy'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T10:59:34.786092Z","iopub.execute_input":"2026-05-04T10:59:34.786376Z","iopub.status.idle":"2026-05-04T10:59:34.900365Z","shell.execute_reply.started":"2026-05-04T10:59:34.786355Z","shell.execute_reply":"2026-05-04T10:59:34.899749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_loss, val_accuracy = model2.evaluate(val_image_gen)\n\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:00:19.875325Z","iopub.execute_input":"2026-05-04T11:00:19.875599Z","iopub.status.idle":"2026-05-04T11:00:26.424392Z","shell.execute_reply.started":"2026-05-04T11:00:19.875577Z","shell.execute_reply":"2026-05-04T11:00:26.423758Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_probs2 = model2.predict(val_image_gen, batch_size = batch_size, verbose = 1)\n\nval_preds2 = (val_probs2.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:01:52.746294Z","iopub.execute_input":"2026-05-04T11:01:52.747245Z","iopub.status.idle":"2026-05-04T11:02:00.015847Z","shell.execute_reply.started":"2026-05-04T11:01:52.747196Z","shell.execute_reply":"2026-05-04T11:02:00.015217Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_true = val_image_gen.labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:01:15.925317Z","iopub.execute_input":"2026-05-04T11:01:15.926205Z","iopub.status.idle":"2026-05-04T11:01:15.930635Z","shell.execute_reply.started":"2026-05-04T11:01:15.926177Z","shell.execute_reply":"2026-05-04T11:01:15.930057Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(classification_report(\n    y_true,\n    val_preds2,\n    target_names=['cat', 'dog']\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:02:02.276814Z","iopub.execute_input":"2026-05-04T11:02:02.277251Z","iopub.status.idle":"2026-05-04T11:02:02.294921Z","shell.execute_reply.started":"2026-05-04T11:02:02.277221Z","shell.execute_reply":"2026-05-04T11:02:02.294083Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm2 = confusion_matrix(y_true, val_preds2)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm2, annot=True, fmt='d',\n            xticklabels=['cat', 'dog'],\n            yticklabels=['cat', 'dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:02:24.260195Z","iopub.execute_input":"2026-05-04T11:02:24.260947Z","iopub.status.idle":"2026-05-04T11:02:24.388149Z","shell.execute_reply.started":"2026-05-04T11:02:24.260914Z","shell.execute_reply":"2026-05-04T11:02:24.387192Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Based on this simple error analysis, we can clearly see that this model has a worse performance then the baseline model that we first did, which highlights the importance of the parameters and callback for our prediction objective","metadata":{}},{"cell_type":"markdown","source":"## Prediction 2","metadata":{}},{"cell_type":"code","source":"test_sample = test_data.iloc[:10].copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:02:59.359748Z","iopub.execute_input":"2026-05-04T11:02:59.360793Z","iopub.status.idle":"2026-05-04T11:02:59.364551Z","shell.execute_reply.started":"2026-05-04T11:02:59.360757Z","shell.execute_reply":"2026-05-04T11:02:59.363912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_probs = model2.predict(sample_gen)\n\nsample_preds = (sample_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:03:20.112330Z","iopub.execute_input":"2026-05-04T11:03:20.112594Z","iopub.status.idle":"2026-05-04T11:03:20.732980Z","shell.execute_reply.started":"2026-05-04T11:03:20.112574Z","shell.execute_reply":"2026-05-04T11:03:20.732330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labels = ['cat', 'dog']\n\n\nplt.figure(figsize=(15, 6))\n\nfor i in range(10):\n    img = plt.imread(\n        f\"/kaggle/working/dogs-vs-cats/test1/{test_sample.iloc[i]['filename']}\"\n    )\n    \n    plt.subplot(2, 5, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    pred_label = labels[sample_preds[i]]\n    prob = sample_probs[i][0]\n    \n    plt.title(f\"{pred_label}\\nprob={prob:.2f}\")\n\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-04T11:03:37.271787Z","iopub.execute_input":"2026-05-04T11:03:37.272512Z","iopub.status.idle":"2026-05-04T11:03:38.044138Z","shell.execute_reply.started":"2026-05-04T11:03:37.272482Z","shell.execute_reply":"2026-05-04T11:03:38.043345Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model 3 - MobileNetV2","metadata":{}},{"cell_type":"markdown","source":"Here, the idea is to use a pretrained model that is already good at classifying images based on their features, in order to get a better performing model then the baseline, while also improving the speed that the model need to train. We choose to use MobileNetV2, which is a deep learning architecture for vision applications.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras import layers\nfrom tensorflow.keras.applications import MobileNetV2\n\nbase_model = MobileNetV2(\n    input_shape=(224, 224, 3),\n    include_top=False,\n    weights='imagenet'\n)\n\nbase_model.trainable = False\n\nmodel3 = keras.Sequential([\n    base_model,\n    layers.GlobalAveragePooling2D(),\n    layers.Dropout(0.2),\n    layers.Dense(1, activation='sigmoid')\n])\n\nmodel3.compile(\n    optimizer='adam',\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nmodel3.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:15:26.534777Z","iopub.execute_input":"2026-05-05T12:15:26.535652Z","iopub.status.idle":"2026-05-05T12:15:27.240707Z","shell.execute_reply.started":"2026-05-05T12:15:26.535618Z","shell.execute_reply":"2026-05-05T12:15:27.240130Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.applications.mobilenet_v2 import preprocess_input\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\ntrain_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    rotation_range=30,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    zoom_range=0.2,\n    horizontal_flip=True\n)\n\nval_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:15:32.714965Z","iopub.execute_input":"2026-05-05T12:15:32.715667Z","iopub.status.idle":"2026-05-05T12:15:32.720063Z","shell.execute_reply.started":"2026-05-05T12:15:32.715635Z","shell.execute_reply":"2026-05-05T12:15:32.719278Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_image_gen3 = train_datagen.flow_from_dataframe(\n    X_train,\n    '/kaggle/working/dogs-vs-cats/train',\n    x_col='filename',\n    y_col='label',\n    target_size=(224, 224),\n    batch_size=32,\n    class_mode='binary',\n    shuffle=True\n)\n\nval_image_gen3 = val_datagen.flow_from_dataframe(\n    X_val,\n    '/kaggle/working/dogs-vs-cats/train',\n    x_col='filename',\n    y_col='label',\n    target_size=(224, 224),\n    batch_size=32,\n    class_mode='binary',\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:15:34.444955Z","iopub.execute_input":"2026-05-05T12:15:34.445497Z","iopub.status.idle":"2026-05-05T12:15:34.652956Z","shell.execute_reply.started":"2026-05-05T12:15:34.445468Z","shell.execute_reply":"2026-05-05T12:15:34.652019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings('ignore')\n\nresults3 = model3.fit(train_image_gen3,epochs=6, validation_data=val_image_gen3,\n                   verbose = 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:15:41.867533Z","iopub.execute_input":"2026-05-05T12:15:41.868231Z","iopub.status.idle":"2026-05-05T12:37:48.008408Z","shell.execute_reply.started":"2026-05-05T12:15:41.868189Z","shell.execute_reply":"2026-05-05T12:37:48.007751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Here, we can clearly see that we get a much better accuracy, even with a lower amount of epochs then for the baseline model. ","metadata":{}},{"cell_type":"markdown","source":"## Error analysis 3","metadata":{}},{"cell_type":"code","source":"results3.history['accuracy']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:37:55.559645Z","iopub.execute_input":"2026-05-05T12:37:55.560341Z","iopub.status.idle":"2026-05-05T12:37:55.564970Z","shell.execute_reply.started":"2026-05-05T12:37:55.560309Z","shell.execute_reply":"2026-05-05T12:37:55.564378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_loss, val_accuracy = model3.evaluate(val_image_gen3)\n\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:38:00.192918Z","iopub.execute_input":"2026-05-05T12:38:00.193203Z","iopub.status.idle":"2026-05-05T12:38:09.215876Z","shell.execute_reply.started":"2026-05-05T12:38:00.193179Z","shell.execute_reply":"2026-05-05T12:38:09.215224Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_probs = model3.predict(val_image_gen3, batch_size = 32, verbose = 1)\n\nval_preds = (val_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:38:20.639084Z","iopub.execute_input":"2026-05-05T12:38:20.639693Z","iopub.status.idle":"2026-05-05T12:38:38.823194Z","shell.execute_reply.started":"2026-05-05T12:38:20.639665Z","shell.execute_reply":"2026-05-05T12:38:38.822263Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\ny_true = val_image_gen3.labels\n\nprint(classification_report(\n    y_true,\n    val_preds,\n    target_names=['cat', 'dog']\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:38:45.038044Z","iopub.execute_input":"2026-05-05T12:38:45.038368Z","iopub.status.idle":"2026-05-05T12:38:45.056334Z","shell.execute_reply.started":"2026-05-05T12:38:45.038342Z","shell.execute_reply":"2026-05-05T12:38:45.055654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm = confusion_matrix(y_true, val_preds)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm, annot=True, fmt='d',\n            xticklabels=['cat', 'dog'],\n            yticklabels=['cat', 'dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:39:06.890453Z","iopub.execute_input":"2026-05-05T12:39:06.890860Z","iopub.status.idle":"2026-05-05T12:39:07.016346Z","shell.execute_reply.started":"2026-05-05T12:39:06.890831Z","shell.execute_reply":"2026-05-05T12:39:07.015587Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"As we can see, the model is already very accurate, and much better performing then the baseline, with near perfecct prediction. ","metadata":{}},{"cell_type":"markdown","source":"Since the number of wrong predictions is low, lets check some of the misclassified predictions as well as the misclassified images where the model was sure it got the right prediciton, even though it didn't","metadata":{}},{"cell_type":"code","source":"mis_idx = np.where(y_true != val_preds)[0]\nprint(\"Number of misclassified images:\", len(mis_idx))\n\nplt.figure(figsize=(12, 12))\nlabel_map = {0: 'cat', 1: 'dog'}\n\nfor i, idx in enumerate(mis_idx[:8]):\n    img_path = os.path.join('/kaggle/working/dogs-vs-cats/train', X_val.iloc[idx]['filename'])\n    img = plt.imread(img_path)\n\n    true_label = label_map[y_true[idx]]\n    pred_label = label_map[val_preds[idx]]\n    prob = val_probs[idx][0]\n\n    plt.subplot(2, 4, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(f\"True: {true_label}\\nPred: {pred_label}\\nProb: {prob:.2f}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:39:45.553557Z","iopub.execute_input":"2026-05-05T12:39:45.554222Z","iopub.status.idle":"2026-05-05T12:39:46.216740Z","shell.execute_reply.started":"2026-05-05T12:39:45.554144Z","shell.execute_reply":"2026-05-05T12:39:46.215906Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"wrong_confidences = []\n\nfor idx in mis_idx:\n    prob = val_probs[idx]\n    pred_class = val_preds[idx]\n    \n    confidence = prob if pred_class == 1 else (1 - prob)\n    confidence = float(confidence)\n    wrong_confidences.append((idx, confidence))\n\nwrong_confidences = sorted(wrong_confidences, key=lambda x: x[1], reverse=True)\n\nplt.figure(figsize=(12, 12))\n\nfor i, (idx, confidence) in enumerate(wrong_confidences[:8]):\n    img_path = os.path.join('/kaggle/working/dogs-vs-cats/train', X_val.iloc[idx]['filename'])\n    img = plt.imread(img_path)\n\n    true_label = label_map[y_true[idx]]\n    pred_label = label_map[val_preds[idx]]\n    prob = val_probs[idx].item()\n\n    plt.subplot(2, 4, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(\n        f\"True: {true_label}\\nPred: {pred_label}\\nProb: {prob:.2f}\\nConf: {confidence:.2f}\"\n    )\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:44:04.745473Z","iopub.execute_input":"2026-05-05T12:44:04.746095Z","iopub.status.idle":"2026-05-05T12:44:06.287795Z","shell.execute_reply.started":"2026-05-05T12:44:04.746065Z","shell.execute_reply":"2026-05-05T12:44:06.286976Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Here we can observe that, when the model gives a wrong prediction confidently, there is usually a reason why. Either :\n* The picture is blurry, so the model cant see the features clearly\n* The face of the animal might not be fully visible\n* The animal might blend with the background\n* Both animals could be in the picture, and we didnt account for that possibility (the is no option to classify if it detects both animals in the picture)","metadata":{}},{"cell_type":"markdown","source":"## Prediction 3","metadata":{}},{"cell_type":"code","source":"test_sample = test_data.iloc[:20].copy()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)\n\nsample_gen3 = sample_datagen.flow_from_dataframe(\n    test_sample,\n    '/kaggle/working/dogs-vs-cats/test1',\n    x_col='filename',\n    y_col=None,\n    target_size=(224, 224),\n    batch_size=1,\n    class_mode=None,\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:46:43.475813Z","iopub.execute_input":"2026-05-05T12:46:43.476540Z","iopub.status.idle":"2026-05-05T12:46:43.483329Z","shell.execute_reply.started":"2026-05-05T12:46:43.476510Z","shell.execute_reply":"2026-05-05T12:46:43.482682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_probs = model3.predict(sample_gen3)\n\nsample_preds = (sample_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:46:45.596616Z","iopub.execute_input":"2026-05-05T12:46:45.597331Z","iopub.status.idle":"2026-05-05T12:46:56.651421Z","shell.execute_reply.started":"2026-05-05T12:46:45.597300Z","shell.execute_reply":"2026-05-05T12:46:56.650804Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"labels = ['cat', 'dog']\n\n\nplt.figure(figsize=(15, 6))\n\nfor i in range(10):\n    img = plt.imread(\n        f\"/kaggle/working/dogs-vs-cats/test1/{test_sample.iloc[i]['filename']}\"\n    )\n    \n    plt.subplot(2, 5, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    \n    pred_label = labels[sample_preds[i]]\n    prob = sample_probs[i][0]\n    \n    plt.title(f\"{pred_label}\\nprob={prob:.2f}\")\n\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T12:47:00.399461Z","iopub.execute_input":"2026-05-05T12:47:00.399864Z","iopub.status.idle":"2026-05-05T12:47:01.126342Z","shell.execute_reply.started":"2026-05-05T12:47:00.399835Z","shell.execute_reply":"2026-05-05T12:47:01.125558Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model 4 - Baseline + HPO","metadata":{}},{"cell_type":"markdown","source":"Since the baseline was the better version of the two CNNs, we will try to do a HPO on it.","metadata":{}},{"cell_type":"code","source":"from tensorflow import keras\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Dense, Flatten, Dropout, BatchNormalization\n\ndef build_model(filters1=32, filters2=64, filters3=64,\n                dense_units=128, dropout_rate=0.3, learning_rate=1e-4):\n\n    model4 = Sequential([\n        Conv2D(filters1, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n        BatchNormalization(),\n        MaxPooling2D(pool_size=(2, 2)),\n        Dropout(dropout_rate),\n\n        Conv2D(filters2, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n        BatchNormalization(),\n        MaxPooling2D(pool_size=(2, 2)),\n        Dropout(dropout_rate),\n\n        Conv2D(filters3, (3, 3), activation='relu', input_shape=(150, 150, 3)),\n        BatchNormalization(),\n        MaxPooling2D(pool_size=(2, 2)),\n        Dropout(dropout_rate),\n\n        Flatten(),\n        Dense(dense_units, activation='relu'),\n        BatchNormalization(),\n        Dropout(dropout_rate + 0.2),\n\n        Dense(1, activation='sigmoid')\n    ])\n\n    model4.compile(\n        optimizer=keras.optimizers.Adam(learning_rate=learning_rate),\n        loss='binary_crossentropy',\n        metrics=['accuracy']\n    )\n\n    return model4","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:42:13.181765Z","iopub.execute_input":"2026-05-05T07:42:13.182583Z","iopub.status.idle":"2026-05-05T07:42:35.257464Z","shell.execute_reply.started":"2026-05-05T07:42:13.182550Z","shell.execute_reply":"2026-05-05T07:42:35.256667Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Hyperparameter optimimization","metadata":{}},{"cell_type":"markdown","source":"This is the chosen search space for this optimization. Each time, we change only one parameter to see how it affects the training.","metadata":{}},{"cell_type":"code","source":"search_space = [\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64,  \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64,  \"dense_units\": 256, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 128, \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64, \"dense_units\": 128, \"dropout_rate\": 0.4, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64, \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 5e-5, \"batch_size\": 32},\n\n    {\"filters1\": 64, \"filters2\": 64,  \"filters3\": 64,  \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 128,  \"filters3\": 64,  \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64, \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 64},\n    {\"filters1\": 32, \"filters2\": 64,  \"filters3\": 64, \"dense_units\": 128, \"dropout_rate\": 0.3, \"learning_rate\": 1e-4, \"batch_size\": 16},\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:42:55.916548Z","iopub.execute_input":"2026-05-05T07:42:55.917505Z","iopub.status.idle":"2026-05-05T07:42:55.923262Z","shell.execute_reply.started":"2026-05-05T07:42:55.917473Z","shell.execute_reply":"2026-05-05T07:42:55.922460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\n\ndef make_generators(batch_size):\n    train_datagen = ImageDataGenerator(\n        rescale=1/255,\n        rotation_range=30,\n        width_shift_range=0.1,\n        height_shift_range=0.1,\n        shear_range=0.2,\n        zoom_range=0.2,\n        horizontal_flip=True,\n        fill_mode='nearest'\n    )\n\n    val_datagen = ImageDataGenerator(rescale=1/255)\n\n    train_image_gen = train_datagen.flow_from_dataframe(\n        X_train,\n        '/kaggle/working/dogs-vs-cats/train',\n        x_col='filename',\n        y_col='label',\n        target_size=(150, 150),\n        batch_size=batch_size,\n        class_mode='binary',\n        shuffle=True\n    )\n\n    val_image_gen = val_datagen.flow_from_dataframe(\n        X_val,\n        '/kaggle/working/dogs-vs-cats/train',\n        x_col='filename',\n        y_col='label',\n        target_size=(150, 150),\n        batch_size=batch_size,\n        class_mode='binary',\n        shuffle=False\n    )\n\n    return train_image_gen, val_image_gen","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:42:58.663929Z","iopub.execute_input":"2026-05-05T07:42:58.664337Z","iopub.status.idle":"2026-05-05T07:42:58.672280Z","shell.execute_reply.started":"2026-05-05T07:42:58.664310Z","shell.execute_reply":"2026-05-05T07:42:58.671408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\n\nhpo_results = []\n\nfor i, params in enumerate(search_space):\n    print(f\"\\nTrial {i+1}/{len(search_space)}\")\n    print(params)\n\n    tf.keras.backend.clear_session()\n\n    train_image_gen4, val_image_gen4 = make_generators(params[\"batch_size\"])\n\n    model_hpo = build_model(\n        filters1=params[\"filters1\"],\n        filters2=params[\"filters2\"],\n        filters3=params[\"filters3\"],\n        dense_units=params[\"dense_units\"],\n        dropout_rate=params[\"dropout_rate\"],\n        learning_rate=params[\"learning_rate\"]\n    )\n\n    history = model_hpo.fit(\n        train_image_gen4,\n        validation_data=val_image_gen4,\n        epochs=4,\n        verbose=1\n    )\n\n    best_val_acc = max(history.history[\"val_accuracy\"])\n\n    hpo_results.append({\n        \"params\": params,\n        \"best_val_accuracy\": best_val_acc\n    })\n\n    print(f\"Best val_accuracy: {best_val_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T07:43:02.078640Z","iopub.execute_input":"2026-05-05T07:43:02.079269Z","iopub.status.idle":"2026-05-05T08:56:29.297458Z","shell.execute_reply.started":"2026-05-05T07:43:02.079241Z","shell.execute_reply":"2026-05-05T08:56:29.296773Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_result = max(hpo_results, key=lambda x: x[\"best_val_accuracy\"])\n\nprint(\"Best configuration:\")\nprint(best_result[\"params\"])\nprint(\"Best validation accuracy:\", best_result[\"best_val_accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T08:57:18.561745Z","iopub.execute_input":"2026-05-05T08:57:18.562515Z","iopub.status.idle":"2026-05-05T08:57:18.566775Z","shell.execute_reply.started":"2026-05-05T08:57:18.562485Z","shell.execute_reply":"2026-05-05T08:57:18.566086Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training with best params","metadata":{}},{"cell_type":"markdown","source":"Once we have the best configuration according to the HPO done above, we can now train the model once again with those parameters and see how it performs","metadata":{}},{"cell_type":"code","source":"best_params = best_result[\"params\"]\n\ntrain_image_gen4, val_image_gen4 = make_generators(best_params[\"batch_size\"])\n\nbest_model = build_model(\n    filters1=best_params[\"filters1\"],\n    filters2=best_params[\"filters2\"],\n    filters3=best_params[\"filters3\"],\n    dense_units=best_params[\"dense_units\"],\n    dropout_rate=best_params[\"dropout_rate\"],\n    learning_rate=best_params[\"learning_rate\"]\n)\n\nhistory = best_model.fit(\n    train_image_gen4,\n    validation_data=val_image_gen4,\n    epochs=16,\n    callbacks=[early_stop, lr_reduction],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:00:24.274512Z","iopub.execute_input":"2026-05-05T09:00:24.275467Z","iopub.status.idle":"2026-05-05T09:20:34.948048Z","shell.execute_reply.started":"2026-05-05T09:00:24.275435Z","shell.execute_reply":"2026-05-05T09:20:34.947317Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Additional training  ---->","metadata":{}},{"cell_type":"code","source":"history = best_model.fit(\n    train_image_gen4,\n    validation_data=val_image_gen4,\n    epochs=10,\n    callbacks=[early_stop, lr_reduction],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:34:52.769633Z","iopub.execute_input":"2026-05-05T09:34:52.770310Z","iopub.status.idle":"2026-05-05T09:48:53.315253Z","shell.execute_reply.started":"2026-05-05T09:34:52.770278Z","shell.execute_reply":"2026-05-05T09:48:53.314608Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Error analysis 4","metadata":{}},{"cell_type":"markdown","source":"The tuned model performed slightly worse than the baseline, indicating that the original setup was already a strong configuration for this task or that we didnt do enough epochs to correctly test the hyperparameters.","metadata":{}},{"cell_type":"code","source":"history.history['accuracy']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:51:47.774341Z","iopub.execute_input":"2026-05-05T09:51:47.775099Z","iopub.status.idle":"2026-05-05T09:51:47.780020Z","shell.execute_reply.started":"2026-05-05T09:51:47.775069Z","shell.execute_reply":"2026-05-05T09:51:47.779395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_loss, val_accuracy = best_model.evaluate(val_image_gen4)\n\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:52:24.533699Z","iopub.execute_input":"2026-05-05T09:52:24.534320Z","iopub.status.idle":"2026-05-05T09:52:31.303953Z","shell.execute_reply.started":"2026-05-05T09:52:24.534289Z","shell.execute_reply":"2026-05-05T09:52:31.303383Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_probs = best_model.predict(val_image_gen4, batch_size = 32, verbose = 1)\n\nval_preds = (val_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:52:48.998753Z","iopub.execute_input":"2026-05-05T09:52:48.999002Z","iopub.status.idle":"2026-05-05T09:52:56.731136Z","shell.execute_reply.started":"2026-05-05T09:52:48.998980Z","shell.execute_reply":"2026-05-05T09:52:56.730451Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_true = val_image_gen4.labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:53:01.077396Z","iopub.execute_input":"2026-05-05T09:53:01.077651Z","iopub.status.idle":"2026-05-05T09:53:01.082034Z","shell.execute_reply.started":"2026-05-05T09:53:01.077629Z","shell.execute_reply":"2026-05-05T09:53:01.081416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(classification_report(\n    y_true,\n    val_preds,\n    target_names=['cat', 'dog']\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:53:12.113315Z","iopub.execute_input":"2026-05-05T09:53:12.114009Z","iopub.status.idle":"2026-05-05T09:53:12.130097Z","shell.execute_reply.started":"2026-05-05T09:53:12.113979Z","shell.execute_reply":"2026-05-05T09:53:12.129514Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm4 = confusion_matrix(y_true, val_preds)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm4, annot=True, fmt='d',\n            xticklabels=['cat', 'dog'],\n            yticklabels=['cat', 'dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:53:53.462086Z","iopub.execute_input":"2026-05-05T09:53:53.462369Z","iopub.status.idle":"2026-05-05T09:53:53.587075Z","shell.execute_reply.started":"2026-05-05T09:53:53.462345Z","shell.execute_reply":"2026-05-05T09:53:53.586458Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The tuned model predicts very badly when the true label is a dog. Cat predictions seem to remain the same.","metadata":{}},{"cell_type":"code","source":"mis_idx = np.where(y_true != val_preds)[0]\nprint(\"Number of misclassified images:\", len(mis_idx))\n\nplt.figure(figsize=(12, 12))\nlabel_map = {0: 'cat', 1: 'dog'}\n\nfor i, idx in enumerate(mis_idx[:8]):\n    img_path = os.path.join('/kaggle/working/dogs-vs-cats/train', X_val.iloc[idx]['filename'])\n    img = plt.imread(img_path)\n\n    true_label = label_map[y_true[idx]]\n    pred_label = label_map[val_preds[idx]]\n    prob = val_probs[idx][0]\n\n    plt.subplot(2, 4, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(f\"True: {true_label}\\nPred: {pred_label}\\nProb: {prob:.2f}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T09:56:35.349494Z","iopub.execute_input":"2026-05-05T09:56:35.349748Z","iopub.status.idle":"2026-05-05T09:56:36.044800Z","shell.execute_reply.started":"2026-05-05T09:56:35.349727Z","shell.execute_reply":"2026-05-05T09:56:36.043988Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model 5 - MobileNetV2 + HPO","metadata":{}},{"cell_type":"markdown","source":"Since the MobileNetV2 model was the best performing one, I thought i might try to do a HPO on that one as well.","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input\n\ndef make_mobilenet_generators(batch_size):\n    train_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_input,\n        rotation_range=30,\n        width_shift_range=0.1,\n        height_shift_range=0.1,\n        zoom_range=0.2,\n        horizontal_flip=True\n    )\n\n    val_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_input\n    )\n\n    train_image_gen = train_datagen.flow_from_dataframe(\n        X_train,\n        '/kaggle/working/dogs-vs-cats/train',\n        x_col='filename',\n        y_col='label',\n        target_size=(150, 150),\n        batch_size=batch_size,\n        class_mode='binary',\n        shuffle=True\n    )\n\n    val_image_gen = val_datagen.flow_from_dataframe(\n        X_val,\n        '/kaggle/working/dogs-vs-cats/train',\n        x_col='filename',\n        y_col='label',\n        target_size=(150, 150),\n        batch_size=batch_size,\n        class_mode='binary',\n        shuffle=False\n    )\n\n    return train_image_gen, val_image_gen","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T10:06:26.404334Z","iopub.execute_input":"2026-05-05T10:06:26.404820Z","iopub.status.idle":"2026-05-05T10:06:26.418660Z","shell.execute_reply.started":"2026-05-05T10:06:26.404788Z","shell.execute_reply":"2026-05-05T10:06:26.418035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras import layers\nfrom tensorflow.keras.applications import MobileNetV2\n\ndef build_mobilenet_model(dropout_rate=0.2, dense_units=128, learning_rate=1e-4):\n    base_model = MobileNetV2(\n        input_shape=(150, 150, 3),\n        include_top=False,\n        weights='imagenet'\n    )\n    \n    base_model.trainable = False\n\n    model = keras.Sequential([\n        base_model,\n        layers.GlobalAveragePooling2D(),\n        layers.Dense(dense_units, activation='relu'),\n        layers.Dropout(dropout_rate),\n        layers.Dense(1, activation='sigmoid')\n    ])\n\n    model.compile(\n        optimizer=keras.optimizers.Adam(learning_rate=learning_rate),\n        loss='binary_crossentropy',\n        metrics=['accuracy']\n    )\n\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T10:10:57.698410Z","iopub.execute_input":"2026-05-05T10:10:57.698665Z","iopub.status.idle":"2026-05-05T10:10:57.704309Z","shell.execute_reply.started":"2026-05-05T10:10:57.698643Z","shell.execute_reply":"2026-05-05T10:10:57.703501Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## HPO","metadata":{}},{"cell_type":"markdown","source":"This is the chosen search space for the optimization of the MobileNetV2 model. Each time, we change only one parameter to see how it affects the training.","metadata":{}},{"cell_type":"code","source":"search_space_mn = [\n    {\"dropout_rate\": 0.2, \"dense_units\": 64,  \"learning_rate\": 1e-4, \"batch_size\": 16},\n    {\"dropout_rate\": 0.2, \"dense_units\": 128, \"learning_rate\": 1e-4, \"batch_size\": 16},\n    {\"dropout_rate\": 0.3, \"dense_units\": 128, \"learning_rate\": 1e-4, \"batch_size\": 16},\n    {\"dropout_rate\": 0.2, \"dense_units\": 128, \"learning_rate\": 5e-5, \"batch_size\": 16},\n    \n    {\"dropout_rate\": 0.2, \"dense_units\": 64,  \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"dropout_rate\": 0.2, \"dense_units\": 128, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"dropout_rate\": 0.3, \"dense_units\": 128, \"learning_rate\": 1e-4, \"batch_size\": 32},\n    {\"dropout_rate\": 0.2, \"dense_units\": 128, \"learning_rate\": 5e-5, \"batch_size\": 32},\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T10:10:59.253946Z","iopub.execute_input":"2026-05-05T10:10:59.254633Z","iopub.status.idle":"2026-05-05T10:10:59.259407Z","shell.execute_reply.started":"2026-05-05T10:10:59.254603Z","shell.execute_reply":"2026-05-05T10:10:59.258787Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mobilenet_hpo_results = []\n\nfor i, params in enumerate(search_space_mn):\n    print(f\"\\nTrial {i+1}/{len(search_space_mn)}\")\n    print(params)\n\n    tf.keras.backend.clear_session()\n\n    train_image_gen5, val_image_gen5 = make_mobilenet_generators(params[\"batch_size\"])\n\n    model_mnet = build_mobilenet_model(\n        dropout_rate=params[\"dropout_rate\"],\n        dense_units=params[\"dense_units\"],\n        learning_rate=params[\"learning_rate\"]\n    )\n\n    history = model_mnet.fit(\n        train_image_gen5,\n        validation_data=val_image_gen5,\n        epochs=2,\n        verbose=1\n    )\n\n    best_val_acc = max(history.history[\"val_accuracy\"])\n\n    mobilenet_hpo_results.append({\n        \"params\": params,\n        \"best_val_accuracy\": best_val_acc\n    })\n\n    print(f\"Best val_accuracy: {best_val_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T10:14:27.554039Z","iopub.execute_input":"2026-05-05T10:14:27.554666Z","iopub.status.idle":"2026-05-05T10:48:50.322497Z","shell.execute_reply.started":"2026-05-05T10:14:27.554636Z","shell.execute_reply":"2026-05-05T10:48:50.321870Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_mobilenet_result = max(mobilenet_hpo_results, key=lambda x: x[\"best_val_accuracy\"])\n\nprint(\"Best MobileNetV2 configuration:\")\nprint(best_mobilenet_result[\"params\"])\nprint(\"Best validation accuracy:\", best_mobilenet_result[\"best_val_accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:23:12.288856Z","iopub.execute_input":"2026-05-05T11:23:12.289151Z","iopub.status.idle":"2026-05-05T11:23:12.294404Z","shell.execute_reply.started":"2026-05-05T11:23:12.289128Z","shell.execute_reply":"2026-05-05T11:23:12.293615Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training with best params 2","metadata":{}},{"cell_type":"markdown","source":"Once we have the best configuration according to the HPO, we can train our model again with those parameters.","metadata":{}},{"cell_type":"code","source":"best_params = best_mobilenet_result[\"params\"]\n\ntrain_image_gen, val_image_gen = make_mobilenet_generators(best_params[\"batch_size\"])\n\nbest_mobilenet_model = build_mobilenet_model(\n    dropout_rate=best_params[\"dropout_rate\"],\n    dense_units=best_params[\"dense_units\"],\n    learning_rate=best_params[\"learning_rate\"]\n)\n\nhistory = best_mobilenet_model.fit(\n    train_image_gen,\n    validation_data=val_image_gen,\n    epochs=8,\n    callbacks=[early_stop, lr_reduction],\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:23:40.565213Z","iopub.execute_input":"2026-05-05T11:23:40.565629Z","iopub.status.idle":"2026-05-05T11:40:14.902423Z","shell.execute_reply.started":"2026-05-05T11:23:40.565599Z","shell.execute_reply":"2026-05-05T11:40:14.901715Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Error analysis 5","metadata":{}},{"cell_type":"code","source":"val_loss, val_accuracy = best_mobilenet_model.evaluate(val_image_gen)\n\nprint(f\"Validation Loss: {val_loss:.4f}\")\nprint(f\"Validation Accuracy: {val_accuracy:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:51:43.026719Z","iopub.execute_input":"2026-05-05T11:51:43.027551Z","iopub.status.idle":"2026-05-05T11:51:53.383077Z","shell.execute_reply.started":"2026-05-05T11:51:43.027517Z","shell.execute_reply":"2026-05-05T11:51:53.382488Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_probs = best_mobilenet_model.predict(val_image_gen, batch_size = 32, verbose = 1)\n\nval_preds = (val_probs.flatten() > 0.5).astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:55:28.735588Z","iopub.execute_input":"2026-05-05T11:55:28.736429Z","iopub.status.idle":"2026-05-05T11:55:44.138514Z","shell.execute_reply.started":"2026-05-05T11:55:28.736392Z","shell.execute_reply":"2026-05-05T11:55:44.137526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"y_true = val_image_gen.labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:55:44.139943Z","iopub.execute_input":"2026-05-05T11:55:44.140287Z","iopub.status.idle":"2026-05-05T11:55:44.144225Z","shell.execute_reply.started":"2026-05-05T11:55:44.140244Z","shell.execute_reply":"2026-05-05T11:55:44.143418Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\n\nprint(classification_report(\n    y_true,\n    val_preds,\n    target_names=['cat', 'dog']\n))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:55:44.145532Z","iopub.execute_input":"2026-05-05T11:55:44.145900Z","iopub.status.idle":"2026-05-05T11:55:44.167725Z","shell.execute_reply.started":"2026-05-05T11:55:44.145866Z","shell.execute_reply":"2026-05-05T11:55:44.167059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix\nimport seaborn as sns\nimport matplotlib.pyplot as plt\n\ncm5 = confusion_matrix(y_true, val_preds)\n\nplt.figure(figsize=(5,4))\nsns.heatmap(cm5, annot=True, fmt='d',\n            xticklabels=['cat', 'dog'],\n            yticklabels=['cat', 'dog'])\nplt.xlabel('Predicted')\nplt.ylabel('True')\nplt.title('Confusion Matrix')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:56:21.093142Z","iopub.execute_input":"2026-05-05T11:56:21.093602Z","iopub.status.idle":"2026-05-05T11:56:21.209038Z","shell.execute_reply.started":"2026-05-05T11:56:21.093574Z","shell.execute_reply":"2026-05-05T11:56:21.208431Z"}},"outputs":[],"execution_count":null},{"cell_type":"raw","source":"","metadata":{}},{"cell_type":"markdown","source":"The performance seems to be more or less the same as the model without HPO.","metadata":{}},{"cell_type":"code","source":"mis_idx = np.where(y_true != val_preds)[0]\nprint(\"Number of misclassified images:\", len(mis_idx))\n\nplt.figure(figsize=(12, 12))\nlabel_map = {0: 'cat', 1: 'dog'}\n\nfor i, idx in enumerate(mis_idx[:8]):\n    img_path = os.path.join('/kaggle/working/dogs-vs-cats/train', X_val.iloc[idx]['filename'])\n    img = plt.imread(img_path)\n\n    true_label = label_map[y_true[idx]]\n    pred_label = label_map[val_preds[idx]]\n    prob = val_probs[idx][0]\n\n    plt.subplot(2, 4, i + 1)\n    plt.imshow(img)\n    plt.axis('off')\n    plt.title(f\"True: {true_label}\\nPred: {pred_label}\\nProb: {prob:.2f}\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-05T11:56:56.001716Z","iopub.execute_input":"2026-05-05T11:56:56.002167Z","iopub.status.idle":"2026-05-05T11:56:56.670895Z","shell.execute_reply.started":"2026-05-05T11:56:56.002126Z","shell.execute_reply":"2026-05-05T11:56:56.670112Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Conclusion","metadata":{}},{"cell_type":"markdown","source":"In this project, I built and compared several deep learning models for the Dogs vs. Cats image classification problem. The goal was to classify each image as either a cat or a dog using CNNs and/or a pretrained model.\n\nI started by preparing the dataset, extracting the labels from the image filenames, and splitting the data into training and validation sets. I also used image augmentation on the training data to help the model generalize better and reduce overfitting. The validation data was kept unchanged so that the model could be evaluated on images that were not artificially modified.\n\nThe first models were built as baseline CNN models. These helped establish an initial performance level and showed how a custom convolutional neural network can learn useful image features. However, the more advanced model used MobileNetV2 with transfer learning. This model performed better because it was already pretrained on a large image dataset.\n\nAfter comparing the models, the MobileNetV2 model gave the best validation performance overall. It achieved stronger accuracy and better classification results than the earlier CNN models, making it the best model in this notebook.\n\nTo improve the results even further, the MobileNetV2 model could be tuned more carefully. For example, more hyperparameter combinations could be tested, such as different learning rates, dropout rates, dense layer sizes, batch sizes, and numbers of training epochs. Another possible improvement would be to unfreeze some of the later layers of MobileNetV2 and fine-tune them on the cats and dogs dataset. This could allow the model to adapt more specifically to this classification task and possibly achieve an even more precise result. I could have also added a table where i compare all 5 models at once.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}