{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.15","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"tpu1vmV38","dataSources":[{"sourceId":21154,"databundleVersionId":1243559,"sourceType":"competition"}],"dockerImageVersionId":30806,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🌸 Flower Classification with TPU - Beginner Friendly\n\nHi there! 👋 I'm Sheema, and in this notebook, I'm going to walk you through how to build a flower image classifier using TensorFlow and a pretrained model. We'll use Kaggle’s free TPU to speed up training ⚡\n\nWhether you're new to deep learning or just exploring TPUs, don't worry — I’ll explain everything step by step, just like we're learning together. Let's get started! 🚀\n","metadata":{}},{"cell_type":"code","source":"# Import Libraries\n\n# Basic Python utilities\nimport os\nimport re\nimport math\nimport random\nimport numpy as np\nfrom sklearn.metrics import f1_score\n\n# Deep Learning using TensorFlow\nimport tensorflow as tf\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras import layers\nfrom tensorflow.keras.layers import Dense\nfrom tensorflow.keras.callbacks import ModelCheckpoint\n\n# Kaggle utility to access TPU datasets\nfrom kaggle_datasets import KaggleDatasets\n\n# For data visualization\nimport matplotlib.pyplot as plt\n%matplotlib inline  # Plots will appear directly in the notebook\nfrom sklearn.metrics import confusion_matrix, classification_report, f1_score\nimport seaborn as sns\n\n\n# ======================\n# Check TensorFlow Version\n# ======================\nprint(f\"TensorFlow version: {tf.__version__}\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:19:42.933083Z","iopub.execute_input":"2025-06-18T01:19:42.933991Z","iopub.status.idle":"2025-06-18T01:19:42.942558Z","shell.execute_reply.started":"2025-06-18T01:19:42.933952Z","shell.execute_reply":"2025-06-18T01:19:42.941174Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🔧 Set up TPU for Fast Training (if available)","metadata":{}},{"cell_type":"code","source":"# Let TensorFlow decide the best way to load data in parallel\nAUTO = tf.data.experimental.AUTOTUNE\n\n#  Try to detect a TPU. If found, use it!\ntry:\n    tpu = tf.distribute.cluster_resolver.TPUClusterResolver()  # This line tries to find the TPU\n    print(\"✅ Found TPU at:\", tpu.master())\nexcept ValueError:\n    tpu = None\n    print(\"❌ No TPU found, falling back to CPU/GPU\")\n\n#  Initialize TPU system if available\nif tpu:\n    tf.config.experimental_connect_to_cluster(tpu)\n    tf.tpu.experimental.initialize_tpu_system(tpu)\n    strategy = tf.distribute.experimental.TPUStrategy(tpu)  # Use TPU for training\nelse:\n    strategy = tf.distribute.get_strategy()  # Default strategy for CPU or GPU\n\n# Print how many replicas (parallel workers) are available\nprint(\" Number of training replicas in sync:\", strategy.num_replicas_in_sync)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:35.156851Z","iopub.execute_input":"2025-06-17T23:07:35.157169Z","iopub.status.idle":"2025-06-17T23:07:43.081797Z","shell.execute_reply.started":"2025-06-17T23:07:35.157140Z","shell.execute_reply":"2025-06-17T23:07:43.080960Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load Dataset Path from Google Cloud (TPU-friendly)","metadata":{}},{"cell_type":"code","source":"#  NOTE: TPUs can't read from local paths directly!\n# So we use KaggleDatasets to get the path on Google Cloud Storage (GCS)\nGCS_DS_PATH = KaggleDatasets().get_gcs_path()\n\nprint(\"📦 GCS Dataset Path:\", GCS_DS_PATH)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:43.082977Z","iopub.execute_input":"2025-06-17T23:07:43.083293Z","iopub.status.idle":"2025-06-17T23:07:43.087296Z","shell.execute_reply.started":"2025-06-17T23:07:43.083268Z","shell.execute_reply":"2025-06-17T23:07:43.086593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":" ### Configuration – Image Size, Batch, Epochs","metadata":{}},{"cell_type":"code","source":"# We'll resize all images to 512x512\n#  Note: This size is heavy for GPU, but TPU can handle it easily!\nIMAGE_SIZE = [512, 512]\n\n# 🔁 We'll train for 20 epochs\nEPOCHS = 20\n\n# Dynamic batch size depending on how many TPU cores we have\nBATCH_SIZE = 16 * strategy.num_replicas_in_sync\nprint(\" Batch size per step:\", BATCH_SIZE)\n\n# ====================================================\n# Choose Correct Dataset Path Based on Image Size\n# ====================================================\n\n#  Kaggle has datasets in multiple resolutions, we pick based on image size\nGCS_PATH_SELECT = {\n    192: GCS_DS_PATH + '/tfrecords-jpeg-192x192',\n    224: GCS_DS_PATH + '/tfrecords-jpeg-224x224',\n    331: GCS_DS_PATH + '/tfrecords-jpeg-331x331',\n    512: GCS_DS_PATH + '/tfrecords-jpeg-512x512'\n}\n\n# Select the path for 512x512 images\nGCS_PATH = GCS_PATH_SELECT[IMAGE_SIZE[0]]\nprint(\" Using dataset from:\", GCS_PATH)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:45.700147Z","iopub.execute_input":"2025-06-17T23:07:45.700854Z","iopub.status.idle":"2025-06-17T23:07:45.705990Z","shell.execute_reply.started":"2025-06-17T23:07:45.700814Z","shell.execute_reply":"2025-06-17T23:07:45.705195Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load the File Paths (.tfrec) for Training/Validation/Test","metadata":{}},{"cell_type":"code","source":"TRAINING_FILENAMES   = tf.io.gfile.glob(GCS_PATH + '/train/*.tfrec')\nVALIDATION_FILENAMES = tf.io.gfile.glob(GCS_PATH + '/val/*.tfrec')\nTEST_FILENAMES       = tf.io.gfile.glob(GCS_PATH + '/test/*.tfrec')\n\nprint(f\" Found {len(TRAINING_FILENAMES)} training files\")\nprint(f\" Found {len(VALIDATION_FILENAMES)} validation files\")\nprint(f\" Found {len(TEST_FILENAMES)} test files\")\n\n#  For reproducibility\nSEED = 101\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:48.500038Z","iopub.execute_input":"2025-06-17T23:07:48.500808Z","iopub.status.idle":"2025-06-17T23:07:48.568836Z","shell.execute_reply.started":"2025-06-17T23:07:48.500767Z","shell.execute_reply":"2025-06-17T23:07:48.567935Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Custom Data Augmentation – Random Erasing","metadata":{}},{"cell_type":"code","source":"#  This function randomly removes a part of the image (like a blackout),\n# so that the model learns to focus on multiple features, not just one part.\n\ndef random_erasing(img, sl=0.1, sh=0.2, rl=0.4, p=0.3):\n    # Get image height, width, and channels\n    h = tf.shape(img)[0]\n    w = tf.shape(img)[1]\n    c = tf.shape(img)[2]\n\n    #  Total area of the image\n    origin_area = tf.cast(h * w, tf.float32)\n\n    #  Calculate possible range for erase patch size\n    e_size_l = tf.cast(tf.round(tf.sqrt(origin_area * sl * rl)), tf.int32)\n    e_size_h = tf.cast(tf.round(tf.sqrt(origin_area * sh / rl)), tf.int32)\n\n    # Make sure erase patch doesn't exceed image size\n    e_height_h = tf.minimum(e_size_h, h)\n    e_width_h  = tf.minimum(e_size_h, w)\n\n    # Randomly select actual erase height and width\n    erase_height = tf.random.uniform([], e_size_l, e_height_h, dtype=tf.int32)\n    erase_width  = tf.random.uniform([], e_size_l, e_width_h, dtype=tf.int32)\n\n    #  Create a zeroed-out erase area (black patch)\n    erase_area = tf.zeros([erase_height, erase_width, c], dtype=tf.uint8)\n\n    # Randomly select position to apply this patch\n    pad_h = h - erase_height\n    pad_top = tf.random.uniform([], 0, pad_h, dtype=tf.int32)\n    pad_bottom = pad_h - pad_top\n\n    pad_w = w - erase_width\n    pad_left = tf.random.uniform([], 0, pad_w, dtype=tf.int32)\n    pad_right = pad_w - pad_left\n\n    # Pad the erase area to match original image size\n    erase_mask = tf.pad([erase_area], [[0, 0], [pad_top, pad_bottom], [pad_left, pad_right], [0, 0]], constant_values=1)\n    erase_mask = tf.squeeze(erase_mask, axis=0)\n\n    # Multiply original image with mask to \"black out\" area\n    erased_img = tf.multiply(tf.cast(img, tf.float32), tf.cast(erase_mask, tf.float32))\n\n    # Apply erasing with probability p, otherwise return original image\n    return tf.cond(\n        tf.random.uniform([], 0, 1) > p,\n        lambda: tf.cast(img, img.dtype),         # No erasing\n        lambda: tf.cast(erased_img, img.dtype)   # Apply erasing\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:50.452667Z","iopub.execute_input":"2025-06-17T23:07:50.452977Z","iopub.status.idle":"2025-06-17T23:07:50.462497Z","shell.execute_reply.started":"2025-06-17T23:07:50.452949Z","shell.execute_reply":"2025-06-17T23:07:50.461658Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"###  Image Decoding + TFRecord Parsing","metadata":{}},{"cell_type":"code","source":"# This function decodes image bytes into a tensor and resizes it\ndef decode_image(image_data):\n    # Decode JPEG image (3 channels)\n    image = tf.image.decode_jpeg(image_data, channels=3)\n    \n    # Normalize pixels to [0, 1]\n    image = tf.cast(image, tf.float32) / 255.0\n    \n    # Resize image to our fixed size (important for TPU)\n    image = tf.reshape(image, [*IMAGE_SIZE, 3])\n    \n    return image\n\n# Read a labeled TFRecord (contains image and class label)\ndef read_labeled_tfrecord(example):\n    LABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"class\": tf.io.FixedLenFeature([], tf.int64),\n    }\n    # Parse the serialized example\n    example = tf.io.parse_single_example(example, LABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    label = tf.cast(example['class'], tf.int32)\n    return image, label\n\n#  Read an unlabeled TFRecord (for test set – no class, only ID)\ndef read_unlabeled_tfrecord(example):\n    UNLABELED_TFREC_FORMAT = {\n        \"image\": tf.io.FixedLenFeature([], tf.string),\n        \"id\": tf.io.FixedLenFeature([], tf.string),\n    }\n    example = tf.io.parse_single_example(example, UNLABELED_TFREC_FORMAT)\n    image = decode_image(example['image'])\n    idnum = example['id']\n    return image, idnum\n\n# General function to load dataset (training/validation/test)\ndef load_dataset(filenames, labeled=True, ordered=False):\n    options = tf.data.Options()\n    if not ordered:\n        options.experimental_deterministic = False  # Faster by ignoring order\n\n    dataset = tf.data.TFRecordDataset(filenames, num_parallel_reads=AUTO)  # Multi-threaded reading\n    dataset = dataset.with_options(options)\n    \n    # Choose parser based on labeled or not\n    dataset = dataset.map(read_labeled_tfrecord if labeled else read_unlabeled_tfrecord, num_parallel_calls=AUTO)\n    \n    return dataset\n\n# ================================================\n#  Data Augmentation – Flip + Erase 🪄\n# ================================================\ndef data_augment(image, label):\n    # Random horizontal flip\n    image = tf.image.random_flip_left_right(image)\n    \n    # Custom random erase function (we wrote earlier)\n    image = random_erasing(image)\n    \n    return image, label\n\n# =====================================\n#  Create Datasets for Model\n# =====================================\n\ndef get_training_dataset():\n    dataset = load_dataset(TRAINING_FILENAMES, labeled=True)\n    dataset = dataset.map(data_augment, num_parallel_calls=AUTO)  # Apply augmentation\n    dataset = dataset.repeat()  # Repeat for multiple epochs\n    dataset = dataset.shuffle(2048)\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(AUTO)  # Boost performance\n    return dataset\n\ndef get_validation_dataset():\n    dataset = load_dataset(VALIDATION_FILENAMES, labeled=True)\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.cache()  # Cache to speed up validation\n    dataset = dataset.prefetch(AUTO)\n    return dataset\n\ndef get_test_dataset(ordered=False):\n    dataset = load_dataset(TEST_FILENAMES, labeled=False, ordered=ordered)\n    dataset = dataset.batch(BATCH_SIZE)\n    dataset = dataset.prefetch(AUTO)\n    return dataset\n\n#  Count number of images from filenames\ndef count_data_items(filenames):\n    # Extract count from TFRecord file name (e.g., flowers00-230.tfrec → 230)\n    n = [int(re.compile(r\"-([0-9]*)\\.\").search(filename).group(1)) for filename in filenames]\n    return np.sum(n)\n\n# Count total images for each split\nNUM_TRAINING_IMAGES = count_data_items(TRAINING_FILENAMES)\nNUM_VALIDATION_IMAGES = count_data_items(VALIDATION_FILENAMES)\nNUM_TEST_IMAGES = count_data_items(TEST_FILENAMES)\n\n# Calculate steps per epoch for training loop\nSTEPS_PER_EPOCH = NUM_TRAINING_IMAGES // BATCH_SIZE\n\n# Print stats for tracking\nprint(f'Dataset: {NUM_TRAINING_IMAGES} training images, {NUM_VALIDATION_IMAGES} validation images, {NUM_TEST_IMAGES} test images')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:07:54.004107Z","iopub.execute_input":"2025-06-17T23:07:54.004715Z","iopub.status.idle":"2025-06-17T23:07:54.017959Z","shell.execute_reply.started":"2025-06-17T23:07:54.004680Z","shell.execute_reply":"2025-06-17T23:07:54.017189Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Custom Learning Rate Schedule","metadata":{}},{"cell_type":"code","source":"#  Initial learning rate setup\nLR_START = 0.00001   # Starting small\nLR_MAX = 0.00005 * strategy.num_replicas_in_sync  # Scale with TPU cores\nLR_MIN = 0.00001     # End minimum value\nLR_RAMPUP_EPOCHS = 4 # Warm-up period\nLR_SUSTAIN_EPOCHS = 0 # No plateau/sustain phase\nLR_EXP_DECAY = 0.75   # After rampup, how fast we decay\n\n#  Learning Rate Function – returns LR based on current epoch\ndef lr_fn(epoch):\n    if epoch < LR_RAMPUP_EPOCHS:\n        # Phase 1: ramp up from start to max\n        lr = (LR_MAX - LR_START) / LR_RAMPUP_EPOCHS * epoch + LR_START\n    elif epoch < LR_RAMPUP_EPOCHS + LR_SUSTAIN_EPOCHS:\n        # Phase 2: sustain at max LR (not used here)\n        lr = LR_MAX\n    else:\n        # Phase 3: exponential decay from max to min\n        lr = (LR_MAX - LR_MIN) * LR_EXP_DECAY**(epoch - LR_RAMPUP_EPOCHS - LR_SUSTAIN_EPOCHS) + LR_MIN\n    return lr\n\n# 🧠 Callback to apply this custom schedule during training\nlr_callback = tf.keras.callbacks.LearningRateScheduler(lr_fn, verbose=True)\n\n# 🖼️ Visualize the learning rate over epochs\nrng = [i for i in range(EPOCHS)]   # x-axis = epoch numbers\ny = [lr_fn(x) for x in rng]         # y-axis = calculated LR values\nplt.plot(rng, y)\nprint(\"Learning rate schedule: {:.3g} to {:.3g} to {:.3g}\".format(y[0], max(y), y[-1]))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:08:01.225875Z","iopub.execute_input":"2025-06-17T23:08:01.226214Z","iopub.status.idle":"2025-06-17T23:08:01.416124Z","shell.execute_reply.started":"2025-06-17T23:08:01.226182Z","shell.execute_reply":"2025-06-17T23:08:01.415344Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Create Different CNN Models for Flowers","metadata":{}},{"cell_type":"code","source":"# 1. ConvNeXt Base - Powerful modern architecture\ndef get_model_ConvNeXtBase():\n    base_model = tf.keras.applications.ConvNeXtBase(\n        weights='imagenet',           # 🔄 Transfer learning from ImageNet\n        include_top=False,            # 🚫 No final dense layer\n        pooling='avg',                # 📉 Use average pooling\n        input_shape=(*IMAGE_SIZE, 3)  # 🎨 Image shape\n    )\n    x = base_model.output\n    predictions = Dense(104, activation='softmax')(x)  # 🌸 Flower class predictions\n    return Model(inputs=base_model.input, outputs=predictions)\n\n# 2. InceptionResNetV2 - Deeper network with inception modules\ndef get_model_InceptionResNetV2():\n    base_model = tf.keras.applications.InceptionResNetV2(\n        weights='imagenet',\n        include_top=False,\n        pooling='avg',\n        input_shape=(*IMAGE_SIZE, 3)\n    )\n    x = base_model.output\n    predictions = Dense(104, activation='softmax')(x)\n    return Model(inputs=base_model.input, outputs=predictions)\n\n# 3. ResNet50 - A well-known backbone (shallower but stable)\ndef get_model_ResNet():\n    base_model = tf.keras.applications.ResNet50(\n        weights='imagenet',\n        include_top=False,\n        pooling='avg',\n        input_shape=(*IMAGE_SIZE, 3)\n    )\n    x = base_model.output\n    predictions = Dense(104, activation='softmax')(x)\n    return Model(inputs=base_model.input, outputs=predictions)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:08:05.062949Z","iopub.execute_input":"2025-06-17T23:08:05.063276Z","iopub.status.idle":"2025-06-17T23:08:05.070228Z","shell.execute_reply.started":"2025-06-17T23:08:05.063248Z","shell.execute_reply":"2025-06-17T23:08:05.069462Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Model Training Begins! (with TPU/GPU Strategy)","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    \n    # Build the InceptionResNetV2 model (you can swap it)\n    model = get_model_InceptionResNetV2()\n    \n    #  Compile the model with Adam optimizer and proper loss for multiclass classification\n    model.compile(\n        optimizer='adam',\n        loss='sparse_categorical_crossentropy',  # good when labels are integers\n        metrics=['sparse_categorical_accuracy']  # show accuracy per epoch\n    )\n\n    # Start training\n    history = model.fit(\n        get_training_dataset(),                 # training data pipeline\n        steps_per_epoch=STEPS_PER_EPOCH,        #  how many batches per epoch\n        epochs=EPOCHS,                           #  total training epochs\n        validation_data=get_validation_dataset(),  #validate on unseen data\n        callbacks=[\n            lr_callback,  # Learning rate scheduler we defined earlier\n            ModelCheckpoint(\n                filepath='my_InceptionResNetV2.keras',  # save best model only\n                monitor='val_loss', \n                save_best_only=True\n            )\n        ]\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:08:07.841648Z","iopub.execute_input":"2025-06-17T23:08:07.841996Z","iopub.status.idle":"2025-06-17T23:44:45.425302Z","shell.execute_reply.started":"2025-06-17T23:08:07.841965Z","shell.execute_reply":"2025-06-17T23:44:45.424022Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training with ConvNeXtBase Backbone\n\n","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    \n    #  Build the ConvNeXtBase model with pretrained ImageNet weights\n    model = get_model_ConvNeXtBase()\n    \n    #  Compile with Adam optimizer and crossentropy loss for multiclass classification\n    model.compile(\n        optimizer='adam',\n        loss='sparse_categorical_crossentropy',\n        metrics=['sparse_categorical_accuracy']\n    )\n\n    #  Train the model with the training dataset\n    history = model.fit(\n        get_training_dataset(),                      # augmented train data\n        steps_per_epoch=STEPS_PER_EPOCH,             # how many steps in one epoch\n        epochs=EPOCHS,                                # total training cycles\n        validation_data=get_validation_dataset(),    # validate performance on val data\n        callbacks=[\n            lr_callback,                              # dynamic learning rate scheduler\n            ModelCheckpoint(\n                filepath='my_ConvNeXtBase.keras',     # save best checkpoint\n                monitor='val_loss', \n                save_best_only=True\n            )\n        ]\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-17T23:48:49.544644Z","iopub.execute_input":"2025-06-17T23:48:49.545045Z","iopub.status.idle":"2025-06-18T00:27:45.027627Z","shell.execute_reply.started":"2025-06-17T23:48:49.545015Z","shell.execute_reply":"2025-06-18T00:27:45.026607Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training with ResNet-50 Backbone","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    \n    # Build the ResNet-50 model with pretrained ImageNet weights\n    model = get_model_ResNet()\n    \n    # Compile the model\n    model.compile(\n        optimizer='adam',                               # optimizer for gradient descent\n        loss='sparse_categorical_crossentropy',         # classification loss\n        metrics=['sparse_categorical_accuracy']         # track accuracy metric\n    )\n\n    # Train the model on the training dataset\n    history = model.fit(\n        get_training_dataset(),                         # load training data\n        steps_per_epoch=STEPS_PER_EPOCH,                # total batches per epoch\n        epochs=EPOCHS,                                   # total training rounds\n        validation_data=get_validation_dataset(),       # heck model performance on val set\n        callbacks=[\n            lr_callback,                                # change learning rate across epochs\n            ModelCheckpoint(\n                filepath='my_ResNet_50.keras',          # save best model automatically\n                monitor='val_loss', \n                save_best_only=True\n            )\n        ]\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T00:32:41.549073Z","iopub.execute_input":"2025-06-18T00:32:41.549526Z","iopub.status.idle":"2025-06-18T01:00:24.844533Z","shell.execute_reply.started":"2025-06-18T00:32:41.549480Z","shell.execute_reply":"2025-06-18T01:00:24.843464Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Load All Trained Models for Inference / Evaluation","metadata":{}},{"cell_type":"code","source":"with strategy.scope():  # For TPU compatibility, wrap everything inside this scope\n\n    #  Load ConvNeXtBase model and its saved weights\n    model1 = get_model_ConvNeXtBase()\n    model1.load_weights(\"/kaggle/working/my_ConvNeXtBase.keras\")\n\n    # Load InceptionResNetV2 model and its saved weights\n    model2 = get_model_InceptionResNetV2()\n    model2.load_weights(\"/kaggle/working/my_InceptionResNetV2.keras\")\n\n    # Load ResNet-50 model and its saved weights\n    model3 = get_model_ResNet()\n    model3.load_weights(\"/kaggle/working/my_ResNet_50.keras\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:02:15.365286Z","iopub.execute_input":"2025-06-18T01:02:15.365633Z","iopub.status.idle":"2025-06-18T01:03:56.203332Z","shell.execute_reply.started":"2025-06-18T01:02:15.365603Z","shell.execute_reply":"2025-06-18T01:03:56.202204Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Grid Search to Determine Best Ensemble Weights (Alpha, Beta, Gamma) using Macro-F1 Score\n","metadata":{}},{"cell_type":"code","source":"def ensemble_grid_search(model1, model2, model3, val_dataset, num_classes=104, num_val_images=2048, n_steps=50):\n    \"\"\"\n    Perform grid search to find best weights for ensemble of three models using macro F1 score.\n    \n    Args:\n        model1, model2, model3: Trained Keras models.\n        val_dataset: A tf.data.Dataset object of validation data (image, label).\n        num_classes: Total number of target classes. (default=104)\n        num_val_images: Total number of validation samples.\n        n_steps: Number of points between 0 and 1 to search for alpha and beta.\n\n    Returns:\n        best_alpha, best_beta, best_gamma, best_f1_score\n    \"\"\"\n    # Step 1: Separate images and labels\n    images_ds = val_dataset.map(lambda image, label: image)\n    labels_ds = val_dataset.map(lambda image, label: label).unbatch()\n    val_labels = next(iter(labels_ds.batch(num_val_images))).numpy()\n    \n    # Step 2: Get model predictions\n    m1 = model1.predict(images_ds)\n    m2 = model2.predict(images_ds)\n    m3 = model3.predict(images_ds)\n\n    # Step 3: Grid search for best ensemble weights\n    scores = []\n    alphas = np.linspace(0, 1, n_steps)\n    betas = np.linspace(0, 1, n_steps)\n\n    for alpha in alphas:\n        for beta in betas:\n            if alpha + beta > 1:\n                continue\n            gamma = 1 - alpha - beta\n            val_probabilities = alpha * m1 + beta * m2 + gamma * m3\n            val_predictions = np.argmax(val_probabilities, axis=-1)\n            f1 = f1_score(val_labels, val_predictions, labels=range(num_classes), average='macro')\n            scores.append((f1, alpha, beta))\n    \n    # Step 4: Get best scores\n    best_score, best_alpha, best_beta = max(scores, key=lambda x: x[0])\n    best_gamma = 1 - best_alpha - best_beta\n\n    # Print results\n    print(f'✅ Best alpha (model1): {best_alpha:.3f}')\n    print(f'✅ Best beta  (model2): {best_beta:.3f}')\n    print(f'✅ Best gamma (model3): {best_gamma:.3f}')\n    print(f'🎯 Best macro-F1 score: {best_score:.4f}')\n    \n    return best_alpha, best_beta, best_gamma, best_score, m1, m2, m3, val_labels\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:15:44.020802Z","iopub.execute_input":"2025-06-18T01:15:44.021805Z","iopub.status.idle":"2025-06-18T01:15:44.031367Z","shell.execute_reply.started":"2025-06-18T01:15:44.021762Z","shell.execute_reply":"2025-06-18T01:15:44.030341Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_dataset = get_validation_dataset()\n\n#best_alpha, best_beta, best_gamma, best_score = ensemble_grid_search(\n #   model1, model2, model3, val_dataset,\n  #  num_classes=104,\n   # num_val_images=NUM_VALIDATION_IMAGES,  # you already have this\n    #n_steps=50  # grid size\n#)\nbest_alpha, best_beta, best_gamma, best_score, m1, m2, m3, val_labels = ensemble_grid_search(\n    model1, model2, model3, val_dataset,\n    num_classes=104,\n    num_val_images=NUM_VALIDATION_IMAGES,\n    n_steps=50\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:16:02.829234Z","iopub.execute_input":"2025-06-18T01:16:02.830172Z","iopub.status.idle":"2025-06-18T01:18:23.106260Z","shell.execute_reply.started":"2025-06-18T01:16:02.830127Z","shell.execute_reply":"2025-06-18T01:18:23.105106Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🏆 Optimal Ensemble Results\n\nAfter running a weighted grid search across predictions from all three models, the following combination yielded the **best performance** on the validation set:\n\n- ✅ **Best Alpha (ConvNeXtBase):** `0.367`\n- ✅ **Best Beta (InceptionResNetV2):** `0.245`\n- ✅ **Best Gamma (ResNet50):** `0.388`\n- 🎯 **Best Macro-F1 Score:** `0.9573`\n\nThis confirms that the **ensemble model outperforms individual architectures** by leveraging their complementary strengths.\n","metadata":{}},{"cell_type":"markdown","source":"# 🎯 Final Predictions and Model Comparison\nUsing the optimal weights from ensemble search, we now generate final predictions, evaluate them with a confusion matrix and classification report, and compare model-level F1 scores.\n","metadata":{}},{"cell_type":"code","source":"# 📦 Import all needed metrics and plotting tools\nfrom sklearn.metrics import confusion_matrix, classification_report, f1_score\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n# Final predictions using optimal alpha, beta, gamma\nfinal_val_probs = best_alpha * m1 + best_beta * m2 + best_gamma * m3\nfinal_val_preds = np.argmax(final_val_probs, axis=-1)\n\n#  Confusion Matrix\ncm = confusion_matrix(val_labels, final_val_preds)\nplt.figure(figsize=(12, 10))\nsns.heatmap(cm, cmap=\"Blues\", xticklabels=False, yticklabels=False)\nplt.title(\"Confusion Matrix of Ensemble Model\")\nplt.xlabel(\"Predicted\")\nplt.ylabel(\"Actual\")\nplt.show()\n\n#  Classification Report (includes F1-score per class)\nprint(\"📋 Classification Report:\")\nprint(classification_report(val_labels, final_val_preds, digits=3))\n\n# Compare F1 scores of individual models vs ensemble\nf1_m1 = f1_score(val_labels, np.argmax(m1, axis=1), average='macro')\nf1_m2 = f1_score(val_labels, np.argmax(m2, axis=1), average='macro')\nf1_m3 = f1_score(val_labels, np.argmax(m3, axis=1), average='macro')\nf1_ensemble = f1_score(val_labels, final_val_preds, average='macro')\n\n# Ploting comparison\nplt.figure(figsize=(8,5))\nmodel_names = ['ConvNeXt', 'InceptionResNetV2', 'ResNet50', 'Ensemble']\nf1_scores = [f1_m1, f1_m2, f1_m3, f1_ensemble]\nsns.barplot(x=model_names, y=f1_scores, palette=\"viridis\")\nplt.ylabel(\"Macro F1 Score\")\nplt.title(\"Model Comparison\")\nplt.ylim(0, 1)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:19:55.582981Z","iopub.execute_input":"2025-06-18T01:19:55.584045Z","iopub.status.idle":"2025-06-18T01:19:56.080188Z","shell.execute_reply.started":"2025-06-18T01:19:55.584003Z","shell.execute_reply":"2025-06-18T01:19:56.078975Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Inference & Submission File Generation\n\nWe use the optimal ensemble weights (α, β, γ) to make final predictions on the test dataset. These predictions are then formatted into a CSV file for submission.\n\n- ✅ Models used: ConvNeXtBase, InceptionResNetV2, ResNet50\n- ✅ Weights applied: alpha = 0.367, beta = 0.245, gamma = 0.388\n- 📄 Output: `submission.csv`\n","metadata":{}},{"cell_type":"code","source":"# Load test dataset\ntest_dataset = get_test_dataset(ordered=True)\n\n# Get test IDs\ntest_ids = [id_.numpy().decode(\"utf-8\") for id_ in next(iter(test_dataset.map(lambda image, idnum: idnum).unbatch().batch(NUM_TEST_IMAGES)))]\n\n# Get test images\ntest_images = test_dataset.map(lambda image, idnum: image)\n\n# Predict from each model\npred1 = model1.predict(test_images, verbose=1)\npred2 = model2.predict(test_images, verbose=1)\npred3 = model3.predict(test_images, verbose=1)\n\n# Weighted ensemble using best alpha, beta, gamma\nfinal_test_preds = best_alpha * pred1 + best_beta * pred2 + best_gamma * pred3\nfinal_test_labels = np.argmax(final_test_preds, axis=1)\n\n# Create submission DataFrame\nimport pandas as pd\n\nsubmission_df = pd.DataFrame({\n    'id': test_ids,\n    'label': final_test_labels\n})\n\n# Save to CSV\n\n# MUST save in root directory and with exact name!\nsubmission_df.to_csv('/kaggle/working/submission.csv', index=False)\nprint(\"✅ submission.csv successfully created and saved!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:39:09.864594Z","iopub.execute_input":"2025-06-18T01:39:09.865008Z","iopub.status.idle":"2025-06-18T01:43:45.472728Z","shell.execute_reply.started":"2025-06-18T01:39:09.864975Z","shell.execute_reply":"2025-06-18T01:43:45.471370Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-18T01:46:33.242652Z","iopub.execute_input":"2025-06-18T01:46:33.243050Z","iopub.status.idle":"2025-06-18T01:46:33.254057Z","shell.execute_reply.started":"2025-06-18T01:46:33.243018Z","shell.execute_reply":"2025-06-18T01:46:33.252894Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ✅ Conclusion\n\nIn this notebook, we implemented a robust image classification pipeline using an **ensemble of three high-performing models**:  \n**ConvNeXtBase**, **InceptionResNetV2**, and **ResNet50**.\n\nInstead of relying on a single model, we explored **weighted ensembling** to combine their predictions. By performing a **grid search** over weight combinations (alpha, beta, gamma), we discovered the most effective blend that maximized **macro-averaged F1 score** — a crucial metric for imbalanced multi-class tasks.\n\n---\n\n### 🔍 Key Takeaways:\n\n- 📈 **Ensemble performance (F1 = 0.9573)** significantly outperformed all individual models.\n- 🧠 Optimal weights:\n  - ConvNeXtBase (α): `0.367`\n  - InceptionResNetV2 (β): `0.245`\n  - ResNet50 (γ): `0.388`\n- 📊 Visual analysis using a confusion matrix and F1 score comparison further validated our approach.\n\n---\n\n## 💡 Final Thoughts\n\nThis project demonstrates the power of **model ensembling** and systematic experimentation. Even when individual models perform well, combining them intelligently can unlock **higher performance**, **stability**, and **generalization**.\n\nSuch ensemble strategies are not just effective in competitions like **Kaggle**, but also valuable in real-world AI applications where accuracy matters.\n\n> 🚀 **Ensemble Learning = Stronger Together.**\n\n---\n\n## 🤝 Let’s Connect!\n\nIf you enjoyed this notebook or found it helpful, feel free to reach out — I love connecting with fellow AI enthusiasts, Kagglers, and innovators!\n\n- 🌐 [LinkedIn](https://www.linkedin.com/in/sheema-masood/)\n- 📊 [Kaggle Profile](https://www.kaggle.com/sheemamasood)\n- 💻 [GitHub](https://github.com/SheemaMasood381)\n- ✉️ [Email Me](mailto:sheemamasood381@gmail.com)\n\nLet’s learn, build, and grow together! 🌱💡\n","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}