{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":13143678,"sourceType":"datasetVersion","datasetId":8327183}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":23163.814315,"end_time":"2025-10-12T11:00:36.594721","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-10-12T04:34:32.780406","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Dog vs Cat Classification","metadata":{"papermill":{"duration":0.004545,"end_time":"2025-10-12T04:34:36.940140","exception":false,"start_time":"2025-10-12T04:34:36.935595","status":"completed"},"tags":[]}},{"cell_type":"markdown","source":"### Importing the required libraries","metadata":{"papermill":{"duration":0.003515,"end_time":"2025-10-12T04:34:36.947417","exception":false,"start_time":"2025-10-12T04:34:36.943902","status":"completed"},"tags":[]}},{"cell_type":"code","source":"import os, shutil, random\nfrom glob import glob\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nimport pandas as pd\n\nimport tensorflow as tf\nfrom tensorflow.keras import layers, models, optimizers, regularizers, Input, Model\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications import MobileNetV2, EfficientNetB0\nfrom tensorflow.keras.applications.mobilenet_v2 import preprocess_input as mb_preprocess\nfrom tensorflow.keras.applications.efficientnet import preprocess_input as eff_preprocess\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score, classification_report, confusion_matrix\nimport json\nsns.set(style=\"whitegrid\")\n\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:36.955869Z","iopub.status.busy":"2025-10-12T04:34:36.955592Z","iopub.status.idle":"2025-10-12T04:34:52.153589Z","shell.execute_reply":"2025-10-12T04:34:52.152997Z"},"papermill":{"duration":15.204119,"end_time":"2025-10-12T04:34:52.155014","exception":false,"start_time":"2025-10-12T04:34:36.950895","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(tf.__version__)","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:52.164458Z","iopub.status.busy":"2025-10-12T04:34:52.163978Z","iopub.status.idle":"2025-10-12T04:34:52.168340Z","shell.execute_reply":"2025-10-12T04:34:52.167685Z"},"papermill":{"duration":0.010116,"end_time":"2025-10-12T04:34:52.169374","exception":false,"start_time":"2025-10-12T04:34:52.159258","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Configuration","metadata":{"papermill":{"duration":0.003597,"end_time":"2025-10-12T04:34:52.176880","exception":false,"start_time":"2025-10-12T04:34:52.173283","status":"completed"},"tags":[]}},{"cell_type":"code","source":"SEED = 42\nIMG_SIZE = 224\nBATCH_SIZE = 32\nLIMIT_PER_CLASS = None  ## For testing before complete run\nEPOCHS_CUSTOM = 50\nEPOCHS_TL_INIT = 50\nEPOCHS_TL_FINE = 50\nFINE_TUNE_LAST_N = 20 \nINPUT_DIR = \"/kaggle/input/dog-vs-cat\"\nDATA_DIR = \"/kaggle/working/data\"\n\nos.makedirs(DATA_DIR, exist_ok=True)\ntf.random.set_seed(SEED)\nnp.random.seed(SEED)","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:52.185175Z","iopub.status.busy":"2025-10-12T04:34:52.184984Z","iopub.status.idle":"2025-10-12T04:34:52.189301Z","shell.execute_reply":"2025-10-12T04:34:52.188588Z"},"papermill":{"duration":0.009654,"end_time":"2025-10-12T04:34:52.190345","exception":false,"start_time":"2025-10-12T04:34:52.180691","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Data loading and labelling","metadata":{}},{"cell_type":"code","source":"files = sorted(glob(os.path.join(INPUT_DIR, \"train\", \"*.jpg\")))\nprint(\"Raw files found:\", len(files))\n\npairs = []\nfor f in files:\n    fname = os.path.basename(f).lower()\n    if \"cat\" in fname:\n        pairs.append((f, \"cat\"))\n    elif \"dog\" in fname:\n        pairs.append((f, \"dog\"))\n\nif LIMIT_PER_CLASS:\n    cats = [p for p in pairs if p[1] == \"cat\"][:LIMIT_PER_CLASS]\n    dogs = [p for p in pairs if p[1] == \"dog\"][:LIMIT_PER_CLASS]\n    pairs = cats + dogs\n    print(f\"LIMIT_PER_CLASS used: {LIMIT_PER_CLASS} per class -> total {len(pairs)}\")\n\nlabels = [p[1] for p in pairs]\nprint(\"Total images used:\", len(pairs))\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:52.206746Z","iopub.status.busy":"2025-10-12T04:34:52.206503Z","iopub.status.idle":"2025-10-12T04:34:52.559715Z","shell.execute_reply":"2025-10-12T04:34:52.558855Z"},"papermill":{"duration":0.359037,"end_time":"2025-10-12T04:34:52.561217","exception":false,"start_time":"2025-10-12T04:34:52.202180","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Data splitting","metadata":{}},{"cell_type":"code","source":"train_pairs, test_pairs = train_test_split(pairs, test_size=0.2, stratify=labels, random_state=SEED)\ntrain_pairs, val_pairs = train_test_split(train_pairs, test_size=0.125, stratify=[p[1] for p in train_pairs], random_state=SEED)\n\nfor split in [\"train\", \"val\", \"test\"]:\n    for cls in [\"cat\", \"dog\"]:\n        os.makedirs(os.path.join(DATA_DIR, split, cls), exist_ok=True)\n\ndef copy_pairs(p_list, split):\n    for src, cls in p_list:\n        dst = os.path.join(DATA_DIR, split, cls, os.path.basename(src))\n        if not os.path.exists(dst):\n            shutil.copy(src, dst)\n\ncopy_pairs(train_pairs, \"train\")\ncopy_pairs(val_pairs, \"val\")\ncopy_pairs(test_pairs, \"test\")\nprint(\"Data prepared at:\", DATA_DIR)\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:52.577815Z","iopub.status.busy":"2025-10-12T04:34:52.577561Z","iopub.status.idle":"2025-10-12T04:34:52.614559Z","shell.execute_reply":"2025-10-12T04:34:52.613797Z"},"papermill":{"duration":0.043061,"end_time":"2025-10-12T04:34:52.616109","exception":false,"start_time":"2025-10-12T04:34:52.573048","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Checking class balance","metadata":{}},{"cell_type":"code","source":"num_cats = sum(1 for _,lbl in pairs if lbl=='cat')\nnum_dogs = sum(1 for _,lbl in pairs if lbl=='dog')\nprint(f\"Cats: {num_cats}, Dogs: {num_dogs}\")","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:34:52.625176Z","iopub.status.busy":"2025-10-12T04:34:52.624891Z","iopub.status.idle":"2025-10-12T04:37:15.658015Z","shell.execute_reply":"2025-10-12T04:37:15.657178Z"},"papermill":{"duration":143.042369,"end_time":"2025-10-12T04:37:15.662730","exception":false,"start_time":"2025-10-12T04:34:52.620361","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Displaying random images from the dataset","metadata":{}},{"cell_type":"code","source":"def show_random(class_name, n=3):\n    class_files = [p[0] for p in pairs if p[1]==class_name]\n    sampled = random.sample(class_files, min(n, len(class_files)))\n    plt.figure(figsize=(12,3))\n    for i,f in enumerate(sampled):\n        img = Image.open(f)\n        plt.subplot(1, n, i+1)\n        plt.imshow(img)\n        plt.title(class_name)\n        plt.axis('off')\n    plt.show()\n\nshow_random(\"cat\")\nshow_random(\"dog\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Data preparation","metadata":{}},{"cell_type":"code","source":"def create_generators_for_custom(rescale=1./255):\n    train_datagen = ImageDataGenerator(rescale=rescale,\n                                       rotation_range=20, width_shift_range=0.15,\n                                       height_shift_range=0.15, shear_range=0.12,\n                                       zoom_range=0.15, horizontal_flip=True, fill_mode='nearest')\n    val_datagen = ImageDataGenerator(rescale=rescale)\n    train_gen = train_datagen.flow_from_directory(os.path.join(DATA_DIR,\"train\"),\n                                                  target_size=(IMG_SIZE,IMG_SIZE),\n                                                  batch_size=BATCH_SIZE, class_mode='binary',\n                                                  shuffle=True, seed=SEED)\n    val_gen = val_datagen.flow_from_directory(os.path.join(DATA_DIR,\"val\"),\n                                              target_size=(IMG_SIZE,IMG_SIZE),\n                                              batch_size=BATCH_SIZE, class_mode='binary',\n                                              shuffle=False)\n    test_gen = val_datagen.flow_from_directory(os.path.join(DATA_DIR,\"test\"),\n                                               target_size=(IMG_SIZE,IMG_SIZE),\n                                               batch_size=BATCH_SIZE, class_mode='binary',\n                                               shuffle=False)\n    return train_gen, val_gen, test_gen\n\ndef create_generators_for_tl(preprocess_fn):\n    train_datagen = ImageDataGenerator(preprocessing_function=preprocess_fn,\n                                       rotation_range=20, width_shift_range=0.15,\n                                       height_shift_range=0.15, shear_range=0.12,\n                                       zoom_range=0.15, horizontal_flip=True, fill_mode='nearest')\n    val_datagen = ImageDataGenerator(preprocessing_function=preprocess_fn)\n    train_gen = train_datagen.flow_from_directory(os.path.join(DATA_DIR,\"train\"),\n                                                  target_size=(IMG_SIZE,IMG_SIZE),\n                                                  batch_size=BATCH_SIZE, class_mode='binary',\n                                                  shuffle=True, seed=SEED)\n    val_gen = val_datagen.flow_from_directory(os.path.join(DATA_DIR,\"val\"),\n                                              target_size=(IMG_SIZE,IMG_SIZE),\n                                              batch_size=BATCH_SIZE, class_mode='binary',\n                                              shuffle=False)\n    test_gen = val_datagen.flow_from_directory(os.path.join(DATA_DIR,\"test\"),\n                                               target_size=(IMG_SIZE,IMG_SIZE),\n                                               batch_size=BATCH_SIZE, class_mode='binary',\n                                               shuffle=False)\n    return train_gen, val_gen, test_gen\n\ntrain_gen_custom, val_gen_custom, test_gen_custom = create_generators_for_custom()\ntrain_gen_mb, val_gen_mb, test_gen_mb = create_generators_for_tl(mb_preprocess)\ntrain_gen_eff, val_gen_eff, test_gen_eff = create_generators_for_tl(eff_preprocess)\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:37:15.671227Z","iopub.status.busy":"2025-10-12T04:37:15.670960Z","iopub.status.idle":"2025-10-12T04:37:15.840715Z","shell.execute_reply":"2025-10-12T04:37:15.839913Z"},"papermill":{"duration":0.175269,"end_time":"2025-10-12T04:37:15.841824","exception":false,"start_time":"2025-10-12T04:37:15.666555","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Defining callbacks","metadata":{}},{"cell_type":"code","source":"def get_callbacks(name, monitor='val_auc'):\n    weights_filepath = f\"best_{name}.weights.h5\"   # must end with '.weights.h5'\n    es = EarlyStopping(monitor=monitor, patience=5, restore_best_weights=True, verbose=1, mode='max')\n    rlr = ReduceLROnPlateau(monitor=monitor, factor=0.5, patience=3, min_lr=1e-6, verbose=1, mode='max')\n    ckpt = ModelCheckpoint(weights_filepath, monitor=monitor, save_best_only=True, save_weights_only=True, mode='max', verbose=1)\n    return [es, rlr, ckpt]\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:37:15.859453Z","iopub.status.busy":"2025-10-12T04:37:15.858853Z","iopub.status.idle":"2025-10-12T04:37:16.450116Z","shell.execute_reply":"2025-10-12T04:37:16.449329Z"},"papermill":{"duration":0.602358,"end_time":"2025-10-12T04:37:16.456511","exception":false,"start_time":"2025-10-12T04:37:15.854153","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Building CNN models","metadata":{}},{"cell_type":"code","source":"def build_custom_cnn(input_shape=(IMG_SIZE,IMG_SIZE,3)):\n    model = models.Sequential([\n        layers.Conv2D(16,3,padding='same',activation='relu',input_shape=input_shape,kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Conv2D(16,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.MaxPooling2D(), layers.Dropout(0.2),\n\n        layers.Conv2D(32,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Conv2D(32,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.MaxPooling2D(), layers.Dropout(0.3),\n\n        layers.Conv2D(64,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Conv2D(64,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.MaxPooling2D(), layers.Dropout(0.3),\n\n        layers.Conv2D(128,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Conv2D(128,3,padding='same',activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.MaxPooling2D(), layers.Dropout(0.4),\n\n        layers.Flatten(),\n        layers.Dense(256,activation='relu',kernel_regularizer=regularizers.l2(1e-4)),\n        layers.BatchNormalization(),\n        layers.Dropout(0.5),\n        layers.Dense(1, activation='sigmoid')\n    ])\n    model.compile(optimizer=optimizers.Adam(1e-3),\n                  loss='binary_crossentropy',\n                  metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n    return model\n\ndef build_mobilenetv2(input_shape=(IMG_SIZE,IMG_SIZE,3)):\n    base = MobileNetV2(weights='imagenet', include_top=False, input_shape=input_shape)\n    base.trainable = False\n    inputs = Input(shape=input_shape)\n    x = base(inputs, training=False)\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dropout(0.2)(x)\n    outputs = layers.Dense(1, activation='sigmoid')(x)\n    model = Model(inputs, outputs, name=\"MobileNetV2_Classifier\")\n    model.compile(optimizer=optimizers.Adam(1e-3), loss='binary_crossentropy',\n                  metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n    return model, base\n\ndef build_efficientnetb0(input_shape=(IMG_SIZE,IMG_SIZE,3)):\n    base = EfficientNetB0(weights='imagenet', include_top=False, input_shape=input_shape)\n    base.trainable = False\n    inputs = Input(shape=input_shape)\n    x = base(inputs, training=False)\n    x = layers.GlobalAveragePooling2D()(x)\n    x = layers.Dropout(0.2)(x)\n    outputs = layers.Dense(1, activation='sigmoid')(x)\n    model = Model(inputs, outputs, name=\"EfficientNetB0_Classifier\")\n    model.compile(optimizer=optimizers.Adam(1e-3), loss='binary_crossentropy',\n                  metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n    return model, base\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:37:16.514664Z","iopub.status.busy":"2025-10-12T04:37:16.514404Z","iopub.status.idle":"2025-10-12T04:37:17.160809Z","shell.execute_reply":"2025-10-12T04:37:17.160255Z"},"papermill":{"duration":0.662902,"end_time":"2025-10-12T04:37:17.161970","exception":false,"start_time":"2025-10-12T04:37:16.499068","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training the models","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import roc_auc_score\n\ndef train_one(build_fn, name, train_gen, val_gen, test_gen,\n              epochs_init, epochs_fine=None, fine_tune_layers=FINE_TUNE_LAST_N):\n\n    ret = build_fn()\n    if isinstance(ret, tuple) and len(ret)==2:\n        model, base = ret\n    else:\n        model = ret\n        base = None\n\n    callbacks = get_callbacks(name)\n\n    # initial training\n    hist_init = model.fit(train_gen, validation_data=val_gen,\n                          epochs=epochs_init, callbacks=callbacks,\n                          steps_per_epoch=len(train_gen),\n                          validation_steps=len(val_gen))\n    hist = hist_init\n\n    # fine-tuneing for pretrained models\n    if epochs_fine and base is not None:\n        print(f\"[{name}] Fine-tuning: unfreezing last {fine_tune_layers} layers...\")\n        base.trainable = True\n        total = len(base.layers)\n        freeze_until = max(0, total - fine_tune_layers)\n        for i, layer in enumerate(base.layers):\n            layer.trainable = False if i < freeze_until else True\n\n        model.compile(optimizer=optimizers.Adam(1e-5), loss='binary_crossentropy',\n                      metrics=['accuracy', tf.keras.metrics.AUC(name='auc')])\n\n        hist_fine = model.fit(train_gen, validation_data=val_gen,\n                              epochs=epochs_fine, callbacks=callbacks,\n                              steps_per_epoch=len(train_gen),\n                              validation_steps=len(val_gen))\n\n        # merge histories\n        combined = {}\n        for k in set(list(hist_init.history.keys()) + list(hist_fine.history.keys())):\n            combined[k] = hist_init.history.get(k, []) + hist_fine.history.get(k, [])\n        class SimpleHistory: \n            def __init__(self, h): self.history = h\n        hist = SimpleHistory(combined)\n    elif epochs_fine and base is None:\n        print(f\"[{name}] Fine-tune requested but no base model; skipping.\")\n\n    # evaluatinf the models\n    preds = model.predict(test_gen, verbose=0)\n    y_prob = preds.reshape(-1)\n    y_pred = (y_prob > 0.5).astype(int)\n    y_true = test_gen.classes\n    acc = np.mean(y_pred == y_true)\n    auc = roc_auc_score(y_true, y_prob)\n\n    return model, hist, acc, auc, y_pred, y_true\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:37:17.222251Z","iopub.status.busy":"2025-10-12T04:37:17.221999Z","iopub.status.idle":"2025-10-12T04:37:17.225798Z","shell.execute_reply":"2025-10-12T04:37:17.225291Z"},"papermill":{"duration":0.019927,"end_time":"2025-10-12T04:37:17.226810","exception":false,"start_time":"2025-10-12T04:37:17.206883","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Custom CNN","metadata":{}},{"cell_type":"code","source":"tf.keras.backend.clear_session()\ncustom_model, h_custom, acc_custom, auc_custom, pred_custom, true_custom = train_one(\n    build_custom_cnn, \"custom\", train_gen_custom, val_gen_custom, test_gen_custom,\n    epochs_init=EPOCHS_CUSTOM, epochs_fine=None\n)\nprint(\"Custom CNN -> Acc: %.4f, AUC: %.4f\" % (acc_custom, auc_custom))\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T04:37:17.288226Z","iopub.status.busy":"2025-10-12T04:37:17.287769Z","iopub.status.idle":"2025-10-12T04:37:19.787110Z","shell.execute_reply":"2025-10-12T04:37:19.786400Z"},"papermill":{"duration":2.51561,"end_time":"2025-10-12T04:37:19.788276","exception":false,"start_time":"2025-10-12T04:37:17.272666","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Mobilenet","metadata":{}},{"cell_type":"code","source":"tf.keras.backend.clear_session()\nmb_model, h_mb, acc_mb, auc_mb, pred_mb, true_mb = train_one(\n    build_mobilenetv2, \"mobilenet\", train_gen_mb, val_gen_mb, test_gen_mb,\n    epochs_init=EPOCHS_TL_INIT, epochs_fine=EPOCHS_TL_FINE, fine_tune_layers=FINE_TUNE_LAST_N\n)\nprint(\"MobileNetV2 -> Acc: %.4f, AUC: %.4f\" % (acc_mb, auc_mb))\n","metadata":{"execution":{"iopub.execute_input":"2025-10-12T07:11:10.068720Z","iopub.status.busy":"2025-10-12T07:11:10.068427Z","iopub.status.idle":"2025-10-12T07:11:10.074451Z","shell.execute_reply":"2025-10-12T07:11:10.073882Z"},"papermill":{"duration":1.243787,"end_time":"2025-10-12T07:11:10.075525","exception":false,"start_time":"2025-10-12T07:11:08.831738","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Efficientnet","metadata":{}},{"cell_type":"code","source":"tf.keras.backend.clear_session()\neff_model, h_eff, acc_eff, auc_eff, pred_eff, true_eff = train_one(\n    build_efficientnetb0, \"efficientnet\", train_gen_eff, val_gen_eff, test_gen_eff,\n    epochs_init=EPOCHS_TL_INIT, epochs_fine=EPOCHS_TL_FINE, fine_tune_layers=FINE_TUNE_LAST_N\n)\nprint(\"EfficientNetB0 -> Acc: %.4f, AUC: %.4f\" % (acc_eff, auc_eff))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Plotting the training curves","metadata":{}},{"cell_type":"code","source":"def hist_to_dict(h):\n    if h is None: \n        return {}\n    if hasattr(h, \"history\"): \n        return h.history\n    if isinstance(h, dict): \n        return h\n    return getattr(h, \"history\", {})\n\nhc = hist_to_dict(h_custom)\nhm = hist_to_dict(h_mb) \nhe = hist_to_dict(h_eff)\nplt.figure(figsize=(14,6))\n\nplt.subplot(1,2,1)\nif 'accuracy' in hc: \n    plt.plot(hc['accuracy'], label='Custom train', marker='o')\nif 'val_accuracy' in hc: \n    plt.plot(hc['val_accuracy'], label='Custom val', linestyle='--', marker='o')\nif 'accuracy' in hm: \n    plt.plot(hm['accuracy'], label='MobileNet train', marker='s')\nif 'val_accuracy' in hm: \n    plt.plot(hm['val_accuracy'], label='MobileNet val', linestyle='--', marker='s')\nif 'accuracy' in he: \n    plt.plot(he['accuracy'], label='EffNet train', marker='^')\nif 'val_accuracy' in he: \n    plt.plot(he['val_accuracy'], label='EffNet val', linestyle='--', marker='^')\nplt.title(\"Accuracy (train vs val)\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\") \nplt.legend() \nplt.grid(True)\n\nplt.subplot(1,2,2)\nif 'loss' in hc: \n    plt.plot(hc['loss'], label='Custom train', marker='o')\nif 'val_loss' in hc: \n    plt.plot(hc['val_loss'], label='Custom val', linestyle='--', marker='o')\nif 'loss' in hm: \n    plt.plot(hm['loss'], label='MobileNet train', marker='s')\nif 'val_loss' in hm: \n    plt.plot(hm['val_loss'], label='MobileNet val', linestyle='--', marker='s')\nif 'loss' in he: \n    plt.plot(he['loss'], label='EffNet train', marker='^')\nif 'val_loss' in he: \n    plt.plot(he['val_loss'], label='EffNet val', linestyle='--', marker='^')\nplt.title(\"Loss (train vs val)\") \nplt.xlabel(\"Epoch\") \nplt.ylabel(\"Loss\") \nplt.legend() \nplt.grid(True)\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\n\ndef eval_model(model, test_gen, name):\n    y_prob = model.predict(test_gen, verbose=0).reshape(-1)\n    y_pred = (y_prob > 0.5).astype(int)\n    y_true = test_gen.classes\n    acc = np.mean(y_pred == y_true)\n    auc = roc_auc_score(y_true, y_prob)\n    print(f\"\\n=== {name} ===\\nAccuracy: {acc:.4f} | AUC: {auc:.4f}\")\n    print(classification_report(y_true, y_pred, target_names=['Cat','Dog']))\n    cm = confusion_matrix(y_true, y_pred)\n    plt.figure(figsize=(4,3)); sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['Cat','Dog'], yticklabels=['Cat','Dog'])\n    plt.title(f\"Confusion Matrix - {name}\"); plt.xlabel(\"Predicted\"); plt.ylabel(\"True\"); plt.show()\n    return acc, auc\n\nacc_c2, auc_c2 = eval_model(custom_model, test_gen_custom, \"Custom CNN\")\nacc_mb2, auc_mb2 = eval_model(mb_model, test_gen_mb, \"MobileNetV2\")\nacc_eff2, auc_eff2 = eval_model(eff_model, test_gen_eff, \"EfficientNetB0\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Comparing the choosing the best model","metadata":{}},{"cell_type":"code","source":"models_info = {\n    'custom': {'model': custom_model, 'acc': acc_c2, 'auc': auc_c2, 'test_gen': test_gen_custom},\n    'mobilenet': {'model': mb_model, 'acc': acc_mb2, 'auc': auc_mb2, 'test_gen': test_gen_mb},\n    'efficientnet': {'model': eff_model, 'acc': acc_eff2, 'auc': auc_eff2, 'test_gen': test_gen_eff}\n}\n\nsorted_models = sorted(models_info.items(), key=lambda kv: (kv[1]['acc'], kv[1]['auc']), reverse=True)\nbest_key, best_info = sorted_models[0]\nbest_model = best_info['model']\nbest_test_gen = best_info['test_gen']\nprint(\"Best model chosen:\", best_key, f\"(Acc={best_info['acc']:.4f}, AUC={best_info['auc']:.4f})\")\n\n# Saving the best model\nbest_full = \"best_overall.keras\"\nbest_weights = \"best_overall.weights.h5\"\nmeta = {\"model_key\": best_key, \"img_size\": IMG_SIZE}\ntry:\n    best_model.save(best_full)\n    print(\"Saved full best model to\", best_full)\n    best_model.save_weights(best_weights)\n    print(\"Saved weights to\", best_weights)\nexcept Exception as e:\n    print(\"Full save failed:\", e, \"Saving weights only.\")\n    best_model.save_weights(best_weights)\n\nwith open(\"best_overall.meta.json\",\"w\") as f:\n    json.dump(meta, f)\nprint(\"Saved metadata to best_overall.meta.json:\", meta)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Sample prediction","metadata":{}},{"cell_type":"code","source":"def preprocess_path(path, model_key):\n    img = Image.open(path).convert(\"RGB\").resize((IMG_SIZE,IMG_SIZE))\n    arr = np.array(img).astype(np.float32)\n    arr = np.expand_dims(arr, axis=0)\n    if model_key == 'mobilenet':\n        return mb_preprocess(arr)\n    elif model_key == 'efficientnet':\n        return eff_preprocess(arr)\n    else:\n        return arr / 255.0\n\ntest_files = best_test_gen.filepaths\nn = min(8, len(test_files))\nindices = random.sample(range(len(test_files)), n)\n\nplt.figure(figsize=(16,8))\nfor i, idx in enumerate(indices):\n    fp = test_files[idx]\n    actual = \"Dog\" if best_test_gen.classes[idx]==1 else \"Cat\"\n    x = preprocess_path(fp, best_key)\n    prob = float(best_model.predict(x, verbose=0).reshape(-1)[0])\n    pred = \"Dog\" if prob>0.5 else \"Cat\"\n    conf = prob if prob>0.5 else 1.0 - prob\n    plt.subplot(2,4,i+1)\n    plt.imshow(Image.open(fp))\n    plt.title(f\"Actual: {actual}\\nPred: {pred} ({conf*100:.1f}%)\")\n    plt.axis('off')\nplt.suptitle(f\"Sample predictions by best model: {best_key.upper()}\")\nplt.tight_layout(rect=[0,0,1,0.96])\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}