{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":30627,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\n\nfrom pathlib import Path\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n\nprint(\"TensorFlow:\", tf.__version__)\nprint(\"GPU:\", tf.config.list_physical_devices(\"GPU\"))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2026-09-17T08:59:40.139345Z","iopub.execute_input":"2026-09-17T08:59:40.139652Z","iopub.status.idle":"2026-09-17T08:59:40.146675Z","shell.execute_reply.started":"2026-09-17T08:59:40.139629Z","shell.execute_reply":"2026-09-17T08:59:40.145552Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nIMG_SIZE = (224, 224)\nBATCH_SIZE = 32\nNUM_CLASSES = 2\nAUTOTUNE = tf.data.AUTOTUNE\n\nHEAD_EPOCHS = 8\nFINE_TUNE_EPOCHS = 12\nLEARNING_RATE_HEAD = 1e-3\nLEARNING_RATE_FINE = 1e-5\n\nCLASS_NAMES = [\"cat\", \"dog\"]\nLABEL_TO_ID = {\"cat\": 0, \"dog\": 1}\nID_TO_LABEL = {0: \"cat\", 1: \"dog\"}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T08:59:40.148165Z","iopub.execute_input":"2026-09-17T08:59:40.148934Z","iopub.status.idle":"2026-09-17T08:59:40.264738Z","shell.execute_reply.started":"2026-09-17T08:59:40.148880Z","shell.execute_reply":"2026-09-17T08:59:40.263856Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Search common Kaggle locations first, then fall back to /kaggle/input.\nSEARCH_ROOTS = [\n    Path(\"/kaggle/input\"),\n    Path(\"/kaggle/working\"),\n    Path(\".\")\n]\n\nimage_files = []\nfor root in SEARCH_ROOTS:\n    if root.exists():\n        image_files.extend(root.rglob(\"*.jpg\"))\n        image_files.extend(root.rglob(\"*.jpeg\"))\n        image_files.extend(root.rglob(\"*.png\"))\n\n# Keep only labeled dog/cat images.\nrecords = []\nfor p in sorted(set(image_files)):\n    name = p.name.lower()\n    if name.startswith(\"cat.\") or name.startswith(\"dog.\"):\n        label = \"cat\" if name.startswith(\"cat.\") else \"dog\"\n        records.append((str(p), label, LABEL_TO_ID[label]))\n\ndf = pd.DataFrame(records, columns=[\"filepath\", \"label\", \"label_id\"]).drop_duplicates(\"filepath\")\n\nif len(df) == 0:\n    raise FileNotFoundError(\n        \"No cat/dog images found. Add the Dogs vs Cats dataset to the Kaggle notebook input.\"\n    )\n\nprint(\"Total labeled images:\", len(df))\nprint(df[\"label\"].value_counts())\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T08:59:40.267940Z","iopub.execute_input":"2026-09-17T08:59:40.268255Z","iopub.status.idle":"2026-09-17T08:59:58.937925Z","shell.execute_reply.started":"2026-09-17T08:59:40.268230Z","shell.execute_reply":"2026-09-17T08:59:58.937117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Stratified split: 20% test, then 20% of the remaining 80% for validation -> 16% overall.\ntrain_val_df, test_df = train_test_split(\n    df,\n    test_size=0.20,\n    stratify=df[\"label_id\"],\n    random_state=SEED\n)\n\ntrain_df, val_df = train_test_split(\n    train_val_df,\n    test_size=0.20,\n    stratify=train_val_df[\"label_id\"],\n    random_state=SEED\n)\n\ntrain_df = train_df.reset_index(drop=True)\nval_df = val_df.reset_index(drop=True)\ntest_df = test_df.reset_index(drop=True)\n\nprint(f\"Train: {len(train_df)}\")\nprint(f\"Validation: {len(val_df)}\")\nprint(f\"Test: {len(test_df)}\")\n\nassert set(train_df.filepath).isdisjoint(val_df.filepath)\nassert set(train_df.filepath).isdisjoint(test_df.filepath)\nassert set(val_df.filepath).isdisjoint(test_df.filepath)","metadata":{"execution":{"iopub.status.busy":"2026-09-17T08:59:58.939054Z","iopub.execute_input":"2026-09-17T08:59:58.939320Z","iopub.status.idle":"2026-09-17T08:59:58.968740Z","shell.execute_reply.started":"2026-09-17T08:59:58.939298Z","shell.execute_reply":"2026-09-17T08:59:58.967939Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_image(path, label):\n    image = tf.io.read_file(path)\n    image = tf.image.decode_image(image, channels=3, expand_animations=False)\n    image = tf.image.resize(image, IMG_SIZE)\n    image = tf.cast(image, tf.float32)\n    return image, label\n\n\ndef make_dataset(frame, training=False):\n    paths = frame[\"filepath\"].values\n    labels = frame[\"label_id\"].values.astype(np.int32)\n\n    ds = tf.data.Dataset.from_tensor_slices((paths, labels))\n    if training:\n        ds = ds.shuffle(len(frame), seed=SEED, reshuffle_each_iteration=True)\n    ds = ds.map(load_image, num_parallel_calls=AUTOTUNE)\n    ds = ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)\n    return ds\n\ntrain_ds = make_dataset(train_df, training=True)\nval_ds = make_dataset(val_df, training=False)\ntest_ds = make_dataset(test_df, training=False)\n\nprint(train_ds, val_ds, test_ds)","metadata":{"execution":{"iopub.status.busy":"2026-09-17T08:59:58.970527Z","iopub.execute_input":"2026-09-17T08:59:58.971187Z","iopub.status.idle":"2026-09-17T08:59:59.035355Z","shell.execute_reply.started":"2026-09-17T08:59:58.971161Z","shell.execute_reply":"2026-09-17T08:59:59.034566Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"data_augmentation = tf.keras.Sequential([\n    tf.keras.layers.RandomFlip(\"horizontal\"),\n    tf.keras.layers.RandomRotation(0.08),\n    tf.keras.layers.RandomZoom(0.10),\n    tf.keras.layers.RandomContrast(0.10),\n], name=\"data_augmentation\")","metadata":{"execution":{"iopub.status.busy":"2026-09-17T08:59:59.036311Z","iopub.execute_input":"2026-09-17T08:59:59.036650Z","iopub.status.idle":"2026-09-17T08:59:59.046768Z","shell.execute_reply.started":"2026-09-17T08:59:59.036627Z","shell.execute_reply":"2026-09-17T08:59:59.046146Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_transfer_model(base_model, model_name):\n    base_model.trainable = False\n\n    inputs = tf.keras.Input(shape=(*IMG_SIZE, 3), name=\"image\")\n    x = data_augmentation(inputs)\n    x = base_model(x, training=False)\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Dropout(0.35)(x)\n    x = tf.keras.layers.Dense(128, activation=\"relu\")(x)\n    x = tf.keras.layers.Dropout(0.25)(x)\n    outputs = tf.keras.layers.Dense(1, activation=\"sigmoid\", name=\"prediction\")(x)\n\n    model = tf.keras.Model(inputs, outputs, name=model_name)\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE_HEAD),\n        loss=\"binary_crossentropy\",\n        metrics=[tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n                 tf.keras.metrics.AUC(name=\"auc\")]\n    )\n    return model\n\n\nclass LiveTrainingPlot(tf.keras.callbacks.Callback):\n    \"\"\"One persistent figure per model; update the same figure after every epoch.\"\"\"\n\n    def __init__(self, model_label):\n        super().__init__()\n        self.model_label = model_label\n        self.global_epoch = 0\n        self.epochs = []\n        self.train_acc = []\n        self.val_acc = []\n        self.train_loss = []\n        self.val_loss = []\n        self.fig = None\n        self.axes = None\n        self.display_handle = None\n\n    def on_train_begin(self, logs=None):\n        # Create the chart only once. Reuse it for both head training and fine-tuning.\n        if self.fig is None:\n            from IPython.display import display\n            self.fig, self.axes = plt.subplots(1, 2, figsize=(13, 4.5))\n            self.fig.suptitle(f\"{self.model_label} — Training Progress\")\n            self.display_handle = display(self.fig, display_id=True)\n\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        self.global_epoch += 1\n        self.epochs.append(self.global_epoch)\n        self.train_acc.append(logs.get(\"accuracy\"))\n        self.val_acc.append(logs.get(\"val_accuracy\"))\n        self.train_loss.append(logs.get(\"loss\"))\n        self.val_loss.append(logs.get(\"val_loss\"))\n\n        ax_acc, ax_loss = self.axes\n\n        # Update the same axes instead of creating a new figure.\n        ax_acc.clear()\n        ax_loss.clear()\n\n        ax_acc.plot(self.epochs, self.train_acc, marker=\"o\", label=\"Train Accuracy\")\n        ax_acc.plot(self.epochs, self.val_acc, marker=\"o\", label=\"Validation Accuracy\")\n        ax_acc.set_title(\"Accuracy\")\n        ax_acc.set_xlabel(\"Epoch\")\n        ax_acc.set_ylabel(\"Accuracy\")\n        ax_acc.set_ylim(0, 1.0)\n        ax_acc.grid(True, alpha=0.25)\n        ax_acc.legend()\n\n        ax_loss.plot(self.epochs, self.train_loss, marker=\"o\", label=\"Train Loss\")\n        ax_loss.plot(self.epochs, self.val_loss, marker=\"o\", label=\"Validation Loss\")\n        ax_loss.set_title(\"Loss\")\n        ax_loss.set_xlabel(\"Epoch\")\n        ax_loss.set_ylabel(\"Loss\")\n        ax_loss.grid(True, alpha=0.25)\n        ax_loss.legend()\n\n        self.fig.suptitle(f\"{self.model_label} — Training Progress (Epoch {self.global_epoch})\")\n        self.fig.tight_layout()\n\n        if self.display_handle is not None:\n            self.display_handle.update(self.fig)\n\n        print(\n            f\"Epoch {self.global_epoch}: \"\n            f\"accuracy={logs.get('accuracy', float('nan')):.4f}, \"\n            f\"val_accuracy={logs.get('val_accuracy', float('nan')):.4f}, \"\n            f\"loss={logs.get('loss', float('nan')):.4f}, \"\n            f\"val_loss={logs.get('val_loss', float('nan')):.4f}\"\n        )\n\n\ndef make_callbacks(model_name, live_plot):\n    return [\n        live_plot,\n        tf.keras.callbacks.EarlyStopping(\n            monitor=\"val_accuracy\", patience=3, mode=\"max\", restore_best_weights=True\n        ),\n        tf.keras.callbacks.ReduceLROnPlateau(\n            monitor=\"val_loss\", factor=0.25, patience=2, min_lr=1e-7\n        ),\n        tf.keras.callbacks.ModelCheckpoint(\n            f\"{model_name}_best.weights.h5\",\n            monitor=\"val_accuracy\",\n            mode=\"max\",\n            save_best_only=True,\n            save_weights_only=True\n        )\n    ]\n\n\ndef fine_tune(model, unfreeze_from_fraction=0.65):\n    base_model = None\n    for layer in model.layers:\n        if isinstance(layer, tf.keras.Model) and layer.name in [\"efficientnetb0\", \"resnet50v2\", \"mobilenetv2_1.00_224\"]:\n            base_model = layer\n            break\n\n    if base_model is None:\n        base_model = next((layer for layer in model.layers if isinstance(layer, tf.keras.Model)), None)\n\n    if base_model is None:\n        raise RuntimeError(\"Could not locate the pretrained backbone.\")\n\n    base_model.trainable = True\n    freeze_until = int(len(base_model.layers) * unfreeze_from_fraction)\n    for i, layer in enumerate(base_model.layers):\n        layer.trainable = i >= freeze_until\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE_FINE),\n        loss=\"binary_crossentropy\",\n        metrics=[tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n                 tf.keras.metrics.AUC(name=\"auc\")]\n    )\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T08:59:59.048380Z","iopub.execute_input":"2026-09-17T08:59:59.048698Z","iopub.status.idle":"2026-09-17T08:59:59.066712Z","shell.execute_reply.started":"2026-09-17T08:59:59.048667Z","shell.execute_reply":"2026-09-17T08:59:59.065953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"efficientnet_base = tf.keras.applications.EfficientNetB0(\n    include_top=False,\n    weights=\"imagenet\",\n    input_shape=(*IMG_SIZE, 3)\n)\n\nefficientnet_model = build_transfer_model(\n    efficientnet_base,\n    \"EfficientNetB0_DogCat\"\n)\n\nefficientnet_model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T08:59:59.067730Z","iopub.execute_input":"2026-09-17T08:59:59.068039Z","iopub.status.idle":"2026-09-17T09:00:02.121217Z","shell.execute_reply.started":"2026-09-17T08:59:59.068010Z","shell.execute_reply":"2026-09-17T09:00:02.120442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"eff_live_plot = LiveTrainingPlot(\"EfficientNetB0\")\n\nhistory_eff_head = efficientnet_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=HEAD_EPOCHS,\n    callbacks=make_callbacks(\"efficientnetb0_head\", eff_live_plot),\n    verbose=1\n)","metadata":{"execution":{"iopub.status.busy":"2026-09-17T09:00:02.122187Z","iopub.execute_input":"2026-09-17T09:00:02.122492Z","iopub.status.idle":"2026-09-17T09:05:53.751080Z","shell.execute_reply.started":"2026-09-17T09:00:02.122467Z","shell.execute_reply":"2026-09-17T09:05:53.750095Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"efficientnet_model = fine_tune(efficientnet_model, unfreeze_from_fraction=0.65)\n\nhistory_eff_ft = efficientnet_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=FINE_TUNE_EPOCHS,\n    callbacks=make_callbacks(\"efficientnetb0_finetune\", eff_live_plot),\n    verbose=1\n)","metadata":{"execution":{"iopub.status.busy":"2026-09-17T09:05:53.752297Z","iopub.execute_input":"2026-09-17T09:05:53.752579Z","iopub.status.idle":"2026-09-17T09:10:04.461675Z","shell.execute_reply.started":"2026-09-17T09:05:53.752555Z","shell.execute_reply":"2026-09-17T09:10:04.460887Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_xception_model():\n    base = tf.keras.applications.Xception(\n        include_top=False,\n        weights=\"imagenet\",\n        input_shape=(*IMG_SIZE, 3)\n    )\n    base.trainable = False\n\n    inputs = tf.keras.Input(shape=(*IMG_SIZE, 3), name=\"image\")\n    x = data_augmentation(inputs)\n\n    # Xception ImageNet preprocessing: [0,255] -> [-1,1]\n    x = tf.keras.applications.xception.preprocess_input(x)\n    x = base(x, training=False)\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Dropout(0.35)(x)\n    x = tf.keras.layers.Dense(256, activation=\"relu\")(x)\n    x = tf.keras.layers.Dropout(0.25)(x)\n    outputs = tf.keras.layers.Dense(1, activation=\"sigmoid\", name=\"prediction\")(x)\n\n    model = tf.keras.Model(inputs, outputs, name=\"Xception_DogCat\")\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=5e-4),\n        loss=\"binary_crossentropy\",\n        metrics=[\n            tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n            tf.keras.metrics.AUC(name=\"auc\")\n        ]\n    )\n    return model\n\nxception_model = build_xception_model()\nxception_model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:10:04.464184Z","iopub.execute_input":"2026-09-17T09:10:04.464455Z","iopub.status.idle":"2026-09-17T09:10:06.432295Z","shell.execute_reply.started":"2026-09-17T09:10:04.464432Z","shell.execute_reply":"2026-09-17T09:10:06.431298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xception_live_plot = LiveTrainingPlot(\"Xception\")\n\nhistory_xc_head = xception_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=HEAD_EPOCHS,\n    callbacks=make_callbacks(\"xception_head\", xception_live_plot),\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:10:06.433487Z","iopub.execute_input":"2026-09-17T09:10:06.434070Z","iopub.status.idle":"2026-09-17T09:25:08.303608Z","shell.execute_reply.started":"2026-09-17T09:10:06.434035Z","shell.execute_reply":"2026-09-17T09:25:08.302776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xception_model = fine_tune(xception_model, unfreeze_from_fraction=0.65)\n\nhistory_xc_ft = xception_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=FINE_TUNE_EPOCHS,\n    callbacks=make_callbacks(\"xception_finetune\", xception_live_plot),\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:25:08.304797Z","iopub.execute_input":"2026-09-17T09:25:08.305217Z","iopub.status.idle":"2026-09-17T09:43:50.039159Z","shell.execute_reply.started":"2026-09-17T09:25:08.305189Z","shell.execute_reply":"2026-09-17T09:43:50.038097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_densenet_model():\n    base = tf.keras.applications.DenseNet121(\n        include_top=False,\n        weights=\"imagenet\",\n        input_shape=(*IMG_SIZE, 3)\n    )\n    base.trainable = False\n\n    inputs = tf.keras.Input(shape=(*IMG_SIZE, 3), name=\"image\")\n    x = data_augmentation(inputs)\n\n    # DenseNet ImageNet preprocessing: RGB -> BGR and subtract ImageNet mean\n    x = tf.keras.applications.densenet.preprocess_input(x)\n    x = base(x, training=False)\n    x = tf.keras.layers.GlobalAveragePooling2D()(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.Dropout(0.35)(x)\n    x = tf.keras.layers.Dense(256, activation=\"relu\")(x)\n    x = tf.keras.layers.Dropout(0.25)(x)\n    outputs = tf.keras.layers.Dense(1, activation=\"sigmoid\", name=\"prediction\")(x)\n\n    model = tf.keras.Model(inputs, outputs, name=\"DenseNet121_DogCat\")\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=5e-4),\n        loss=\"binary_crossentropy\",\n        metrics=[\n            tf.keras.metrics.BinaryAccuracy(name=\"accuracy\"),\n            tf.keras.metrics.AUC(name=\"auc\")\n        ]\n    )\n    return model\n\ndensenet_model = build_densenet_model()\ndensenet_model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:43:50.040502Z","iopub.execute_input":"2026-09-17T09:43:50.041293Z","iopub.status.idle":"2026-09-17T09:43:54.255869Z","shell.execute_reply.started":"2026-09-17T09:43:50.041267Z","shell.execute_reply":"2026-09-17T09:43:54.255200Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"densenet_live_plot = LiveTrainingPlot(\"DenseNet121\")\n\nhistory_dn_head = densenet_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=HEAD_EPOCHS,\n    callbacks=make_callbacks(\"densenet121_head\", densenet_live_plot),\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:43:54.257037Z","iopub.execute_input":"2026-09-17T09:43:54.257382Z","iopub.status.idle":"2026-09-17T09:53:23.427563Z","shell.execute_reply.started":"2026-09-17T09:43:54.257341Z","shell.execute_reply":"2026-09-17T09:53:23.426751Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"densenet_model = fine_tune(densenet_model, unfreeze_from_fraction=0.65)\n\nhistory_dn_ft = densenet_model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=FINE_TUNE_EPOCHS,\n    callbacks=make_callbacks(\"densenet121_finetune\", densenet_live_plot),\n    verbose=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T09:53:23.428780Z","iopub.execute_input":"2026-09-17T09:53:23.429070Z","iopub.status.idle":"2026-09-17T09:58:10.294220Z","shell.execute_reply.started":"2026-09-17T09:53:23.429045Z","shell.execute_reply":"2026-09-17T09:58:10.293287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def merge_histories(h1, h2):\n    history = {}\n    for key in h1.history.keys():\n        history[key] = h1.history[key] + h2.history.get(key, [])\n    return history\n\n\ndef plot_history(history, title):\n    plt.figure(figsize=(10, 4))\n    plt.plot(history[\"accuracy\"], label=\"train accuracy\")\n    plt.plot(history[\"val_accuracy\"], label=\"validation accuracy\")\n    plt.title(title)\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy\")\n    plt.legend()\n    plt.grid(True)\n    plt.show()\n\nplot_history(merge_histories(history_eff_head, history_eff_ft), \"EfficientNetB0 Accuracy\")\nplot_history(merge_histories(history_xc_head, history_xc_ft), \"Xception Accuracy\")\nplot_history(merge_histories(history_dn_head, history_dn_ft), \"DenseNet121 Accuracy\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:24.387717Z","iopub.execute_input":"2026-09-17T10:03:24.388439Z","iopub.status.idle":"2026-09-17T10:03:25.186334Z","shell.execute_reply.started":"2026-09-17T10:03:24.388404Z","shell.execute_reply":"2026-09-17T10:03:25.185519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def evaluate_model(model, model_name):\n    y_true = test_df[\"label_id\"].to_numpy()\n    prob = model.predict(test_ds, verbose=1).reshape(-1)\n    y_pred = (prob >= 0.5).astype(np.int32)\n\n    acc = accuracy_score(y_true, y_pred)\n    print(\"=\" * 80)\n    print(model_name)\n    print(f\"Test accuracy: {acc:.4f}\")\n    print(\"Meets 0.97 target:\", acc >= 0.97)\n    print(\"\\nClassification Report\")\n    print(classification_report(y_true, y_pred, target_names=CLASS_NAMES, digits=4))\n    print(\"Confusion Matrix\")\n    print(confusion_matrix(y_true, y_pred))\n    print(\"=\" * 80)\n    return acc, y_true, y_pred, prob","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:29.442757Z","iopub.execute_input":"2026-09-17T10:03:29.443547Z","iopub.status.idle":"2026-09-17T10:03:29.449319Z","shell.execute_reply.started":"2026-09-17T10:03:29.443515Z","shell.execute_reply":"2026-09-17T10:03:29.448410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"eff_acc, y_true, eff_pred, eff_prob = evaluate_model(\n    efficientnet_model,\n    \"EfficientNetB0\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:29.821115Z","iopub.execute_input":"2026-09-17T10:03:29.821416Z","iopub.status.idle":"2026-09-17T10:03:42.848358Z","shell.execute_reply.started":"2026-09-17T10:03:29.821393Z","shell.execute_reply":"2026-09-17T10:03:42.847584Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"xception_acc, y_true_xc, xception_pred, xception_prob = evaluate_model(\n    xception_model,\n    \"Xception\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:03:42.849722Z","iopub.execute_input":"2026-09-17T10:03:42.850498Z","iopub.status.idle":"2026-09-17T10:04:12.998110Z","shell.execute_reply.started":"2026-09-17T10:03:42.850469Z","shell.execute_reply":"2026-09-17T10:04:12.997160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"densenet_acc, _, densenet_pred, densenet_prob = evaluate_model(\n    densenet_model,\n    \"DenseNet121\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:12.999164Z","iopub.execute_input":"2026-09-17T10:04:12.999439Z","iopub.status.idle":"2026-09-17T10:04:31.479911Z","shell.execute_reply.started":"2026-09-17T10:04:12.999416Z","shell.execute_reply":"2026-09-17T10:04:31.479150Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = pd.DataFrame({\n    \"Model\": [\"EfficientNetB0\", \"Xception\", \"DenseNet121\"],\n    \"Test Accuracy\": [eff_acc, xception_acc, densenet_acc],\n})\n\nresults[\"Target >= 0.97\"] = results[\"Test Accuracy\"] >= 0.97\n\ndisplay(results.style.format({\"Test Accuracy\": \"{:.4f}\"}))\n\nif not (results[\"Test Accuracy\"] >= 0.97).all():\n    print(\"At least one model is below 0.97. Increase fine-tuning epochs, unfreeze more layers, or use stronger augmentation.\")\nelse:\n    print(\"All 3 models achieved the target test accuracy >= 0.97 on this run.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-17T10:04:31.481485Z","iopub.execute_input":"2026-09-17T10:04:31.481957Z","iopub.status.idle":"2026-09-17T10:04:31.550421Z","shell.execute_reply.started":"2026-09-17T10:04:31.481871Z","shell.execute_reply":"2026-09-17T10:04:31.549528Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}