{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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"},"colab":{"provenance":[],"gpuType":"T4","machine_shape":"hm"},"accelerator":"GPU","kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CM3 Project 1: Deep Computer Vision\n## Dogs vs. Cats Classification\n### Kaggle Link: https://www.kaggle.com/c/dogs-vs-cats\n\n\n\n","metadata":{"id":"8RaBJI2jz20j"}},{"cell_type":"markdown","source":"## Problem Statement\n\n**Goal:** Build a binary classifier that can identify images of cats and dogs.\n\n**Challenge:** Training a CNN from scratch takes is data heavy and time consuiming. Transfer learning (Chapter 14) shortcuts this by starting with models already trained on millions of ImageNet images.\n\n**Approach:** Following the book Hands On Machine Leanring, I'll train two models:\n- **Model A:** Custom CNN trained from random initialization\n- **Model B:** ResNet50 pretrained on ImageNet for cat/dog classification","metadata":{"id":"V2fBUnfLz-SL"}},{"cell_type":"markdown","source":"## Setup\n","metadata":{"id":"1h-n8Eeb0EC2"}},{"cell_type":"code","source":"# Hide warnings\nimport warnings\nwarnings.filterwarnings('ignore')\nimport os\nos.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'\n\n# Common imports\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport os\nimport shutil\nimport tensorflow as tf\nfrom sklearn.metrics import classification_report, confusion_matrix\nimport seaborn as sns\n\n# Suppress TensorFlow warnings\ntf.get_logger().setLevel('ERROR')\n\nnp.random.seed(42)\ntf.random.set_seed(42)\n\n%matplotlib inline\n\nprint(\"Imports loaded!\")","metadata":{"id":"R3SHU1c41viT","outputId":"f947aa75-92c6-4213-9c14-1741a27c1a47","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:12:45.270410Z","iopub.execute_input":"2025-10-27T06:12:45.271058Z","iopub.status.idle":"2025-10-27T06:12:45.284409Z","shell.execute_reply.started":"2025-10-27T06:12:45.271032Z","shell.execute_reply":"2025-10-27T06:12:45.283517Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Kaggle Path Setup\n\n","metadata":{"id":"rWzrTjzS19KT"}},{"cell_type":"code","source":"# Check Kaggle environment\nimport os\n\nprint(\"Running in Kaggle Notebook\")\n\n# Data paths for Dogs vs Cats competition\nBASE_PATH = '/kaggle/input/dogs-vs-cats/'\nTRAIN_ZIP = BASE_PATH + 'train.zip'\nTEST_ZIP = BASE_PATH + 'test1.zip'\n\n# Verify it works\nimport os\nprint(f\"✓ Data found: {os.path.exists(BASE_PATH)}\")\nprint(f\"✓ Contents: {os.listdir(BASE_PATH)}\")\n\nprint(\"Kaggle API already configured!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:12:53.839233Z","iopub.execute_input":"2025-10-27T06:12:53.839746Z","iopub.status.idle":"2025-10-27T06:12:53.846848Z","shell.execute_reply.started":"2025-10-27T06:12:53.839724Z","shell.execute_reply":"2025-10-27T06:12:53.846090Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Download and Preparation\n\nDownloading and organizing the Dogs vs. Cats dataset.","metadata":{"id":"Ujc0hGlY2cOM"}},{"cell_type":"code","source":"# Create directory structure\nimport os\nimport zipfile\nimport shutil\n\n# Use the paths we defined earlier\nTRAIN_ZIP = '/kaggle/input/dogs-vs-cats/train.zip'\nTEST_ZIP = '/kaggle/input/dogs-vs-cats/test1.zip'\n\n# Create working directory\nos.makedirs('/kaggle/working/data', exist_ok=True)\n\n# Extract train data\nprint(\"Extracting training data...\")\nwith zipfile.ZipFile(TRAIN_ZIP, 'r') as zip_ref:\n    zip_ref.extractall('/kaggle/working/data/')\n\n# Extract test data\nprint(\"Extracting test data...\")\nwith zipfile.ZipFile(TEST_ZIP, 'r') as zip_ref:\n    zip_ref.extractall('/kaggle/working/data/')\n\nprint(\"✓ Dataset extracted!\")\nprint(f\"Training images: {len(os.listdir('/kaggle/working/data/train'))}\")\nprint(f\"Test images: {len(os.listdir('/kaggle/working/data/test1'))}\")","metadata":{"id":"xjLwLa4H2evA","outputId":"5937542f-f954-42a5-b43c-f7973e26e01e","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:12:56.842088Z","iopub.execute_input":"2025-10-27T06:12:56.842387Z","iopub.status.idle":"2025-10-27T06:13:06.417483Z","shell.execute_reply.started":"2025-10-27T06:12:56.842367Z","shell.execute_reply":"2025-10-27T06:13:06.416687Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Organize data into train/validation split\norganized_dir = '/kaggle/working/data/organized'\nos.makedirs(f'{organized_dir}/cats', exist_ok=True)\nos.makedirs(f'{organized_dir}/dogs', exist_ok=True)\n\n# Move files to category folders\nprint(\"Organizing by category...\")\nfor filename in os.listdir('/kaggle/working/data/train'):\n    src = f'/kaggle/working/data/train/{filename}'\n    if filename.startswith('cat'):\n        shutil.copy(src, f'{organized_dir}/cats/{filename}')\n    else:\n        shutil.copy(src, f'{organized_dir}/dogs/{filename}')\n\nprint(f\"✓ Organized into cats ({len(os.listdir(organized_dir + '/cats'))}) and dogs ({len(os.listdir(organized_dir + '/dogs'))})\")","metadata":{"id":"l1b1wkrl53IB","outputId":"09ccf860-e338-4f88-a986-de343d69243b","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:10.887645Z","iopub.execute_input":"2025-10-27T06:13:10.887935Z","iopub.status.idle":"2025-10-27T06:13:14.423143Z","shell.execute_reply.started":"2025-10-27T06:13:10.887913Z","shell.execute_reply":"2025-10-27T06:13:14.422303Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Explore the Data","metadata":{"id":"XqUE5uEt6AhE"}},{"cell_type":"code","source":"# Display sample images\nfig, axes = plt.subplots(2, 5, figsize=(15, 6))\n\n# Show some cats\ncat_files = os.listdir(organized_dir + '/cats')[:5]\nfor i, filename in enumerate(cat_files):\n    img_path = os.path.join(organized_dir, 'cats', filename)\n    img = plt.imread(img_path)\n    axes[0, i].imshow(img)\n    axes[0, i].set_title('Cat', fontsize=12)\n    axes[0, i].axis('off')\n\n# Show some dogs\ndog_files = os.listdir(organized_dir + '/dogs')[:5]\nfor i, filename in enumerate(dog_files):\n    img_path = os.path.join(organized_dir, 'dogs', filename)\n    img = plt.imread(img_path)\n    axes[1, i].imshow(img)\n    axes[1, i].set_title('Dog', fontsize=12)\n    axes[1, i].axis('off')\n\nplt.suptitle('Sample Images from Dataset', fontsize=16, y=1.02)\nplt.tight_layout()\nplt.show()","metadata":{"id":"n88lQeh56DpK","outputId":"6bdc2129-2616-4d91-9f48-4ba633d190b4","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:19.594752Z","iopub.execute_input":"2025-10-27T06:13:19.595243Z","iopub.status.idle":"2025-10-27T06:13:20.529419Z","shell.execute_reply.started":"2025-10-27T06:13:19.595222Z","shell.execute_reply":"2025-10-27T06:13:20.528211Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load and Split the Data\n\n","metadata":{"id":"YwYhCMtn6J2O"}},{"cell_type":"code","source":"# Image parameters\nIMG_SIZE = 150\nBATCH_SIZE = 32\n\n# Load datasets with 80/20 split\ntraining_ds = tf.keras.utils.image_dataset_from_directory(\n    'data/organized',\n    validation_split=0.2,\n    subset=\"training\",\n    seed=42,\n    image_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE\n)\n\nvalidation_ds = tf.keras.utils.image_dataset_from_directory(\n    'data/organized',\n    validation_split=0.2,\n    subset=\"validation\",\n    seed=42,\n    image_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE\n)\n\nprint(\"Data loaded\")","metadata":{"id":"aNNRVuBV6Mcc","outputId":"6bcdf4dd-0ecc-4721-823a-cb818d24c8e7","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:25.098595Z","iopub.execute_input":"2025-10-27T06:13:25.099370Z","iopub.status.idle":"2025-10-27T06:13:27.876500Z","shell.execute_reply.started":"2025-10-27T06:13:25.099334Z","shell.execute_reply":"2025-10-27T06:13:27.875928Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Preprocessing","metadata":{"id":"nHASczrBe7ss"}},{"cell_type":"code","source":"# Create preprocessing layer (Chapter 14 approach)\npreprocess = tf.keras.Sequential([\n    tf.keras.layers.Rescaling(1./255)\n])\n\n# Apply preprocessing to datasets\ntraining_ds = training_ds.map(lambda x, y: (preprocess(x), y))\ntraining_ds = training_ds.prefetch(1)\n\nvalidation_ds = validation_ds.map(lambda x, y: (preprocess(x), y))\nvalidation_ds = validation_ds.prefetch(1)\n\nprint(\"Data Normalized\")","metadata":{"id":"KfvooEginMMt","outputId":"0c72476f-d615-4eed-c388-6d42e041055a","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:32.476256Z","iopub.execute_input":"2025-10-27T06:13:32.476984Z","iopub.status.idle":"2025-10-27T06:13:32.514737Z","shell.execute_reply.started":"2025-10-27T06:13:32.476960Z","shell.execute_reply":"2025-10-27T06:13:32.514093Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Visualize Sample Images","metadata":{"id":"2tpfUxxKfiN8"}},{"cell_type":"code","source":"# Display sample images from training set\nplt.figure(figsize=(15, 6))\n\nfor images, labels in training_ds.take(1):\n    for i in range(min(10, len(images))):\n        plt.subplot(2, 5, i + 1)\n        plt.imshow(images[i])\n        plt.title('Dog' if labels[i] == 1 else 'Cat', fontsize=12)\n        plt.axis('off')\n\nplt.suptitle('Sample Images from Training Set (with preprocessing)', fontsize=16)\nplt.tight_layout()\nplt.show()\n\nprint(\"Note: These images have been resized to 224x224 and normalized to [0,1] range\")","metadata":{"id":"Xv_BwweCflqn","outputId":"fa24ef39-0da4-4152-cf85-b2c28ce62632","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:40.534343Z","iopub.execute_input":"2025-10-27T06:13:40.535111Z","iopub.status.idle":"2025-10-27T06:13:41.514118Z","shell.execute_reply.started":"2025-10-27T06:13:40.535087Z","shell.execute_reply":"2025-10-27T06:13:41.513180Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model A: CNN Trained from Scratch\n\nFollowing Chapter 14's approach to building a custom CNN architecture. Tried to keep it simple at first but found I needed more layers.","metadata":{"id":"DtfChwQa6Sak"}},{"cell_type":"code","source":"from functools import partial\n\n# Create a partial function for default Conv2D layers\n# Keep it consistant\nDefaultConv2D = partial(tf.keras.layers.Conv2D,\n                       kernel_size=3,\n                       padding=\"same\",\n                       activation=\"relu\",\n                       kernel_initializer=\"he_normal\")\n\n# NEWER MORE COMPLEX LIKE THE BOOK\ndef create_cnn_from_scratch():\n    \"\"\"\n    Custom CNN architecture\n    Using 4 convolutional blocks with progressive filter increases.\n    \"\"\"\n    model = tf.keras.Sequential([\n        # First convolutional block - Chapter 14 pattern\n        DefaultConv2D(filters=32, input_shape=(IMG_SIZE, IMG_SIZE, 3)),\n        tf.keras.layers.MaxPooling2D(),\n\n        # Second convolutional block\n        DefaultConv2D(filters=64),\n        tf.keras.layers.MaxPooling2D(),\n\n        # Third convolutional block\n        DefaultConv2D(filters=128),\n        tf.keras.layers.MaxPooling2D(),\n\n        # Fourth convolutional block just to follow the book's deeper architecture\n        #tf.keras.layers.Conv2D(filters=256),\n        #tf.keras.layers.MaxPooling2D(),\n\n        # Flatten and dense layers\n        tf.keras.layers.Flatten(),\n        tf.keras.layers.Dropout(0.4),  # Regularization\n        tf.keras.layers.Dense(units=256, activation='relu'),\n        tf.keras.layers.Dropout(0.4),\n        tf.keras.layers.Dense(units=1, activation='sigmoid')  # Binary classification\n    ])\n\n    return model\n\n# Create Model A\nmodel_a = create_cnn_from_scratch()\n\n# Compile with Adam optimizer\nmodel_a.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),  # Standard learning rate\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nprint(\"Model A: CNN from Scratch\")\nmodel_a.summary()\nprint(f\"\\nTotal parameters: {model_a.count_params():,}\")","metadata":{"id":"IsO2Iuwap-dg","outputId":"8cbb6454-7745-400e-c5d8-e3c635b98631","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:53.107275Z","iopub.execute_input":"2025-10-27T06:13:53.108001Z","iopub.status.idle":"2025-10-27T06:13:53.195616Z","shell.execute_reply.started":"2025-10-27T06:13:53.107976Z","shell.execute_reply":"2025-10-27T06:13:53.194892Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train Model A\n\nTraining from scratch typically requires more epochs. Following Chapter 14's pattern with callbacks for early stopping.","metadata":{"id":"6q2_4jeL6aUn"}},{"cell_type":"code","source":"# NEW WAY - TRAINING MODEL A\n# Callbacks\nearly_stopping = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss',\n    patience=5,  # Stop if no improvement for 5 epochs\n    restore_best_weights=True,\n    verbose=1\n)\n\nprint(\"Training Model A (CNN from Scratch)...\")\n# Track training time\nfrom datetime import datetime\nimport time\n\nstart_time = time.time()\nstart_datetime = datetime.now()\n\nhistory_a = model_a.fit(\n    training_ds,\n    epochs=10,\n    validation_data=validation_ds,\n    callbacks=[early_stopping],\n    verbose=1\n)\n\nprint(\"\\nModel A training complete!\")\nend_time = time.time()\nend_datetime = datetime.now()\ntraining_time_a = end_time - start_time\nprint(f\"Stopped at epoch {len(history_a.history['accuracy'])}\")\nprint(f\"Training Time: {training_time_a:.2f} seconds ({training_time_a/60:.2f} minutes)\")","metadata":{"id":"-zIErCJwTx4l","outputId":"17f14eb5-9d7e-49ac-f6bb-4e5578c3b059","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:13:59.472664Z","iopub.execute_input":"2025-10-27T06:13:59.473394Z","iopub.status.idle":"2025-10-27T06:15:42.029620Z","shell.execute_reply.started":"2025-10-27T06:13:59.473361Z","shell.execute_reply":"2025-10-27T06:15:42.028908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **NOTES**:\nStrong case of overfitting. Training hit 93% but validation stuck at around 83%. The model basically memorized the training set instead of learning general cat/dog features. Early stopping caught it before it got worse. Not great, but it could be better.","metadata":{"id":"R-rcWhawtV8Q"}},{"cell_type":"markdown","source":"## Separate Preprocessing for ResNet50\n\nWhy two preprocessing pipelines?\n\nModel A uses standard [0, 1] normalization (dividing by 255). But ResNet50 expects ImageNet-specific preprocessing:\n- Images in [0, 255] range\n- BGR color order + ImageNet mean subtraction\n\n\nUsing tf.keras.applications.resnet50.preprocess_input() fixes this. It matches the ImageNet format ResNet50 was trained on.\n\n","metadata":{"id":"q_8o8503-zMo"}},{"cell_type":"code","source":"print(\"Creating ResNet50-specific datasets...\")\n\n# Load fresh datasets WITHOUT [0,1] normalization (ResNet needs [0, 255])\ntraining_ds_resnet = tf.keras.utils.image_dataset_from_directory(\n    'data/organized',\n    validation_split=0.2,\n    subset=\"training\",\n    seed=42,\n    image_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE\n)\n\nvalidation_ds_resnet = tf.keras.utils.image_dataset_from_directory(\n    'data/organized',\n    validation_split=0.2,\n    subset=\"validation\",\n    seed=42,\n    image_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE\n)\n\n# Define ResNet50 preprocessing function\ndef preprocess_for_resnet50(image, label):\n    # Images are already in [0, 255] range from raw loading\n    # Apply ResNet50's preprocessing (BGR conversion + ImageNet mean subtraction)\n    image = tf.keras.applications.resnet50.preprocess_input(image)\n    return image, label\n\n# Apply preprocessing\ntraining_ds_resnet = training_ds_resnet.map(preprocess_for_resnet50)\ntraining_ds_resnet = training_ds_resnet.prefetch(1)\n\nvalidation_ds_resnet = validation_ds_resnet.map(preprocess_for_resnet50)\nvalidation_ds_resnet = validation_ds_resnet.prefetch(1)\n\nprint(\"ResNet50 datasets ready..\")","metadata":{"id":"qj7FcGra_jRd","outputId":"637381c2-4823-4c3c-9096-08ddbd41435d","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:16:16.524751Z","iopub.execute_input":"2025-10-27T06:16:16.525323Z","iopub.status.idle":"2025-10-27T06:16:19.112568Z","shell.execute_reply.started":"2025-10-27T06:16:16.525298Z","shell.execute_reply":"2025-10-27T06:16:19.111916Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model B: Transfer Learning with ResNet50\n\n","metadata":{"id":"N-quQTCpaMDJ"}},{"cell_type":"code","source":"# Load pretrained tf.keras.applications.ResNet50\nprint(\"Loading pretrained tf.keras.applications.ResNet50 from ImageNet...\")\n\nbase_model = tf.keras.applications.ResNet50(\n    weights='imagenet',  # Use ImageNet pretrained weights\n    include_top=False,   # Exclude the final classification layer\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\n\n# Freeze the base model (only train the new top layers)\nbase_model.trainable = False\n\nprint(f\"Base model loaded: {base_model.name}\")\nprint(f\"Total layers in base: {len(base_model.layers)}\")","metadata":{"id":"NoHl6AXUaPr1","outputId":"33ad5d7d-447f-42f3-c41e-49e4c202a4b2","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:16:24.097363Z","iopub.execute_input":"2025-10-27T06:16:24.098003Z","iopub.status.idle":"2025-10-27T06:16:31.003709Z","shell.execute_reply.started":"2025-10-27T06:16:24.097979Z","shell.execute_reply":"2025-10-27T06:16:31.002922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build Model B with custom classification head\n\n# Build the model page 520\ninputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = base_model(inputs, training=False)  # Note: training=False for inference mode\nx = tf.keras.layers.GlobalAveragePooling2D()(x)  # book recommends this over Flatten\nx = tf.keras.layers.Dense(256, activation='relu')(x)\nx = tf.keras.layers.Dropout(0.5)(x)\noutputs = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n\nmodel_b = tf.keras.Model(inputs, outputs)\n\n# Only the top layers\nmodel_b.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),  # Same as Model A\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nprint(\"Model B: Transfer Learning with tf.keras.applications.ResNet50\")\nmodel_b.summary()\nprint(f\"\\nTrainable parameters: {sum([tf.size(w).numpy() for w in model_b.trainable_weights]):,}\")\nprint(f\"Non-trainable parameters: {sum([tf.size(w).numpy() for w in model_b.non_trainable_weights]):,}\")","metadata":{"id":"HZH45QIUaTS4","outputId":"ac8d1536-9fe0-4567-b6ba-0aea0ef270eb","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:16:36.189566Z","iopub.execute_input":"2025-10-27T06:16:36.190175Z","iopub.status.idle":"2025-10-27T06:16:36.311130Z","shell.execute_reply.started":"2025-10-27T06:16:36.190150Z","shell.execute_reply":"2025-10-27T06:16:36.310356Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train Top Layers Only\n\n","metadata":{"id":"aAC4IolPaX3q"}},{"cell_type":"code","source":"print(\"Training only the top layers...\\n\")\n\n# Same callbacks as Model A\nearly_stopping_b = tf.keras.callbacks.EarlyStopping(\n    monitor='val_loss',\n    patience=5,\n    restore_best_weights=True,\n    verbose=1\n)\n\n#Record timer\nstart_time = time.time()\nstart_datetime = datetime.now()\n\n# Training should converge quickly bec of pretrained features\nhistory_b = model_b.fit(\n    training_ds_resnet,\n    epochs=5,  # Fewer epochs for B, 10 is too long\n    validation_data=validation_ds_resnet,\n    callbacks=[early_stopping_b],\n    verbose=1\n)\n\nprint(\"\\nModel B Training complete!\")\nend_time = time.time()\nend_datetime = datetime.now()\ntraining_time_b = end_time - start_time\nprint(f\"Stopped at epoch {len(history_b.history['accuracy'])}\")\nprint(f\"Training Time: {training_time_b:.2f} seconds ({training_time_b/60:.2f} minutes)\")","metadata":{"id":"Ivh1-zrAaarT","outputId":"cd2c8d67-8b4d-4664-cb15-8c058c08847d","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:16:40.637176Z","iopub.execute_input":"2025-10-27T06:16:40.638021Z","iopub.status.idle":"2025-10-27T06:19:13.396815Z","shell.execute_reply.started":"2025-10-27T06:16:40.637988Z","shell.execute_reply":"2025-10-27T06:19:13.396045Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### **NOTE:**\nStarted at 94% accuracy on epoch 1 and quickly climbed to 98%. No overfitting this time. Train and val accuracy basically matched.","metadata":{"id":"ia0MsfXcuSY2"}},{"cell_type":"markdown","source":"## Model Comparison and Evaluation\n","metadata":{"id":"OJ1ZCIfca1pF"}},{"cell_type":"code","source":"def plot_models_comparison(history_a, history_b):\n    fig, axes = plt.subplots(2, 2, figsize=(15, 10))\n\n    # Model A - Accuracy\n    axes[0, 0].plot(history_a['accuracy'], label='Training', linewidth=2)\n    axes[0, 0].plot(history_a['val_accuracy'], label='Validation', linewidth=2)\n    axes[0, 0].set_title('Model A - CNN from Scratch - Accuracy', fontsize=14, fontweight='bold')\n    axes[0, 0].set_xlabel('Epoch')\n    axes[0, 0].set_ylabel('Accuracy')\n    axes[0, 0].legend()\n    axes[0, 0].grid(True, alpha=0.3)\n\n    # Model A - Loss\n    axes[0, 1].plot(history_a['loss'], label='Training', linewidth=2)\n    axes[0, 1].plot(history_a['val_loss'], label='Validation', linewidth=2)\n    axes[0, 1].set_title('Model A - CNN from Scratch - Loss', fontsize=14, fontweight='bold')\n    axes[0, 1].set_xlabel('Epoch')\n    axes[0, 1].set_ylabel('Loss')\n    axes[0, 1].legend()\n    axes[0, 1].grid(True, alpha=0.3)\n\n    # Model B - Accuracy\n    axes[1, 0].plot(history_b['accuracy'], label='Training', linewidth=2, color='green')\n    axes[1, 0].plot(history_b['val_accuracy'], label='Validation', linewidth=2, color='orange')\n    axes[1, 0].set_title('Model B - Transfer Learning - Accuracy', fontsize=14, fontweight='bold')\n    axes[1, 0].set_xlabel('Epoch')\n    axes[1, 0].set_ylabel('Accuracy')\n    axes[1, 0].legend()\n    axes[1, 0].grid(True, alpha=0.3)\n\n    # Model B - Loss\n    axes[1, 1].plot(history_b['loss'], label='Training', linewidth=2, color='green')\n    axes[1, 1].plot(history_b['val_loss'], label='Validation', linewidth=2, color='orange')\n    axes[1, 1].set_title('Model B - Transfer Learning - Loss', fontsize=14, fontweight='bold')\n    axes[1, 1].set_xlabel('Epoch')\n    axes[1, 1].set_ylabel('Loss')\n    axes[1, 1].legend()\n    axes[1, 1].grid(True, alpha=0.3)\n\n    plt.tight_layout()\n    plt.show()\n\nplot_models_comparison(history_a.history, history_b.history)","metadata":{"id":"uwyWhvU3a5PR","outputId":"cd0d8c45-6163-4e4c-9ae4-c4360ecd81cb","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:19:24.717320Z","iopub.execute_input":"2025-10-27T06:19:24.718021Z","iopub.status.idle":"2025-10-27T06:19:25.455530Z","shell.execute_reply.started":"2025-10-27T06:19:24.717994Z","shell.execute_reply":"2025-10-27T06:19:25.454804Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**MODEL A:**\n* Training: 95%, Validation: 84.3%. It showed some clear overfitting (almost 10% gap).\n* Early stopping restored best weights from epoch 4.\n\n**MODEL B:**\n* Training: 98.3%, Validation: 98.1%. Very little overfitting (0.1% gap).\n* Seems stable throughout all 5 epochs.\n","metadata":{"id":"K0Wt9eswPfEL"}},{"cell_type":"markdown","source":"### Confusion Matrix","metadata":{"id":"jXXI-Q_ca82h"}},{"cell_type":"markdown","source":"**Note:** Used AI assistance to fix this confusion matrix code. I was getting ~50% accuracy in the matrices even though my models showed 82% and 98% during training. The issue was that iterating through the validation datasets multiple times caused shuffling issues. This led to predictions and labels weren't matching up. The solution was to collect both predictions and labels in a single loop to maintain their order.","metadata":{"id":"CDxueTgTyXzy"}},{"cell_type":"code","source":"## Confusion Matrix\n\nfrom sklearn.metrics import confusion_matrix\nimport seaborn as sns\n\n# Model A - collect everything in one loop\ny_true_a, y_pred_a = [], []\nfor images, labels in validation_ds:\n    preds = model_a.predict(images, verbose=0)\n    y_true_a.extend(labels.numpy())\n    y_pred_a.extend((preds > 0.5).astype(int).flatten())\n\n# Model B - collect everything in one loop\ny_true_b, y_pred_b = [], []\nfor images, labels in validation_ds_resnet:\n    preds = model_b.predict(images, verbose=0)\n    y_true_b.extend(labels.numpy())\n    y_pred_b.extend((preds > 0.5).astype(int).flatten())\n\n# Create matrices\ncm_a = confusion_matrix(y_true_a, y_pred_a)\ncm_b = confusion_matrix(y_true_b, y_pred_b)\n\n# Plot\nfig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n\nsns.heatmap(cm_a, annot=True, fmt='d', cmap='Blues', ax=ax1,\n            xticklabels=['Cat', 'Dog'], yticklabels=['Cat', 'Dog'])\nax1.set_title('Model A: From Scratch')\n\nsns.heatmap(cm_b, annot=True, fmt='d', cmap='Greens', ax=ax2,\n            xticklabels=['Cat', 'Dog'], yticklabels=['Cat', 'Dog'])\nax2.set_title('Model B: Transfer Learning')\n\nplt.tight_layout()\nplt.show()","metadata":{"id":"d4eDHjwia-6H","outputId":"72cddde0-275b-474a-b242-44453f3136c7","trusted":true,"execution":{"iopub.status.busy":"2025-10-27T06:19:38.315147Z","iopub.execute_input":"2025-10-27T06:19:38.315765Z","iopub.status.idle":"2025-10-27T06:20:15.078014Z","shell.execute_reply.started":"2025-10-27T06:19:38.315741Z","shell.execute_reply":"2025-10-27T06:20:15.077161Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Reflections\n\nBuilding a CNN from scratch (Model A) involved a lot of trial and error with hyperparameters just to get reasonable results. And even then, it only reached 83% accuracy with severe overfitting. Transfer learning (Model B) achieved 98% accuracy with minimal overfitting and honestly felt easier to work with. The pretrained model had already learned useful features from millions of images, so it basically met me halfway.\n\nThere are a few things I'd like to try next. Fine-tuning some base layers could be interesting, though with 98% accuracy and really low loss, it probably won't make much difference here. I'm more curious about adding data augmentation to make the training more challenging, or trying other pretrained models like MobileNetV2 or EfficientNet to see how they perform.","metadata":{"id":"JLp2y8tLbT6A"}}]}