{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":38284,"databundleVersionId":4155799}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.models import Model\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:22:54.472920Z","iopub.execute_input":"2026-07-06T10:22:54.473156Z","iopub.status.idle":"2026-07-06T10:23:11.880007Z","shell.execute_reply.started":"2026-07-06T10:22:54.473133Z","shell.execute_reply":"2026-07-06T10:23:11.879473Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN_DIR = \"/kaggle/input/4-animal-classification/train\"\nTEST_DIR  = \"/kaggle/input/4-animal-classification/test\"\n\nIMG_SIZE = 299          # Xception native size\nBATCH_SIZE = 16         # smaller batch = better generalization\nEPOCHS = 30\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:11.881735Z","iopub.execute_input":"2026-07-06T10:23:11.882185Z","iopub.status.idle":"2026-07-06T10:23:11.886153Z","shell.execute_reply.started":"2026-07-06T10:23:11.882155Z","shell.execute_reply":"2026-07-06T10:23:11.885330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.applications.xception import preprocess_input\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:11.887029Z","iopub.execute_input":"2026-07-06T10:23:11.887345Z","iopub.status.idle":"2026-07-06T10:23:11.911062Z","shell.execute_reply.started":"2026-07-06T10:23:11.887313Z","shell.execute_reply":"2026-07-06T10:23:11.910416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    validation_split=0.2,\n    rotation_range=25,\n    zoom_range=0.25,\n    width_shift_range=0.1,\n    height_shift_range=0.1,\n    horizontal_flip=True\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:11.911819Z","iopub.execute_input":"2026-07-06T10:23:11.912115Z","iopub.status.idle":"2026-07-06T10:23:11.918086Z","shell.execute_reply.started":"2026-07-06T10:23:11.912083Z","shell.execute_reply":"2026-07-06T10:23:11.917257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"classes = ['cat', 'deer', 'dog', 'horse']\n\ntrain_generator = train_datagen.flow_from_directory(\n    TRAIN_DIR,\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    classes=classes,\n    subset=\"training\"\n)\n\nval_generator = train_datagen.flow_from_directory(\n    TRAIN_DIR,\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    classes=classes,\n    subset=\"validation\"\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:11.918959Z","iopub.execute_input":"2026-07-06T10:23:11.919166Z","iopub.status.idle":"2026-07-06T10:23:13.651030Z","shell.execute_reply.started":"2026-07-06T10:23:11.919136Z","shell.execute_reply":"2026-07-06T10:23:13.650487Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train_generator.class_indices)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:13.651904Z","iopub.execute_input":"2026-07-06T10:23:13.652178Z","iopub.status.idle":"2026-07-06T10:23:13.656509Z","shell.execute_reply.started":"2026-07-06T10:23:13.652147Z","shell.execute_reply":"2026-07-06T10:23:13.655695Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_model = tf.keras.applications.Xception(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\n\nbase_model.trainable = False\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:13.658178Z","iopub.execute_input":"2026-07-06T10:23:13.658880Z","iopub.status.idle":"2026-07-06T10:23:18.047542Z","shell.execute_reply.started":"2026-07-06T10:23:13.658847Z","shell.execute_reply":"2026-07-06T10:23:18.046624Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"x = base_model.output\nx = GlobalAveragePooling2D()(x)\nx = Dense(512, activation=\"relu\")(x)\nx = Dropout(0.5)(x)\noutput = Dense(4, activation=\"softmax\")(x)\n\nmodel = Model(inputs=base_model.input, outputs=output)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:18.048561Z","iopub.execute_input":"2026-07-06T10:23:18.048847Z","iopub.status.idle":"2026-07-06T10:23:18.080551Z","shell.execute_reply.started":"2026-07-06T10:23:18.048819Z","shell.execute_reply":"2026-07-06T10:23:18.080013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=\"adam\",\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:18.081387Z","iopub.execute_input":"2026-07-06T10:23:18.081647Z","iopub.status.idle":"2026-07-06T10:23:18.093634Z","shell.execute_reply.started":"2026-07-06T10:23:18.081620Z","shell.execute_reply":"2026-07-06T10:23:18.092904Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"callbacks = [\n    tf.keras.callbacks.EarlyStopping(\n        monitor=\"val_accuracy\",\n        patience=6,\n        restore_best_weights=True\n    ),\n    tf.keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.3,\n        patience=3,\n        min_lr=1e-6\n    )\n]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:31.506547Z","iopub.execute_input":"2026-07-06T10:23:31.507222Z","iopub.status.idle":"2026-07-06T10:23:31.511690Z","shell.execute_reply.started":"2026-07-06T10:23:31.507191Z","shell.execute_reply":"2026-07-06T10:23:31.510818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history1 = model.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=12,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:23:33.976430Z","iopub.execute_input":"2026-07-06T10:23:33.976812Z","iopub.status.idle":"2026-07-06T10:35:10.468471Z","shell.execute_reply.started":"2026-07-06T10:23:33.976787Z","shell.execute_reply":"2026-07-06T10:35:10.467812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_model.trainable = True\n\nfor layer in base_model.layers[:-40]:\n    layer.trainable = False\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:35:59.334129Z","iopub.execute_input":"2026-07-06T10:35:59.334989Z","iopub.status.idle":"2026-07-06T10:35:59.339668Z","shell.execute_reply.started":"2026-07-06T10:35:59.334943Z","shell.execute_reply":"2026-07-06T10:35:59.338794Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-5),\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:36:01.715455Z","iopub.execute_input":"2026-07-06T10:36:01.716201Z","iopub.status.idle":"2026-07-06T10:36:01.736679Z","shell.execute_reply.started":"2026-07-06T10:36:01.716171Z","shell.execute_reply":"2026-07-06T10:36:01.735978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"history2 = model.fit(\n    train_generator,\n    validation_data=val_generator,\n    epochs=EPOCHS,\n    callbacks=callbacks\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:36:04.384825Z","iopub.execute_input":"2026-07-06T10:36:04.385091Z","iopub.status.idle":"2026-07-06T10:50:04.118321Z","shell.execute_reply.started":"2026-07-06T10:36:04.385068Z","shell.execute_reply":"2026-07-06T10:50:04.117656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tta_datagen = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    horizontal_flip=True\n)\n\ntest_generator = tta_datagen.flow_from_directory(\n    TEST_DIR,\n    target_size=(IMG_SIZE, IMG_SIZE),\n    batch_size=1,\n    class_mode=None,\n    shuffle=False\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:04.120046Z","iopub.execute_input":"2026-07-06T10:50:04.120280Z","iopub.status.idle":"2026-07-06T10:50:05.681200Z","shell.execute_reply.started":"2026-07-06T10:50:04.120258Z","shell.execute_reply":"2026-07-06T10:50:05.680573Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = []\n\nfor _ in range(5):  # 5 TTA passes\n    preds.append(model.predict(test_generator))\n\npredictions = np.mean(preds, axis=0)\npredicted_labels = np.argmax(predictions, axis=1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:05.682501Z","iopub.execute_input":"2026-07-06T10:50:05.682992Z","iopub.status.idle":"2026-07-06T10:50:59.163334Z","shell.execute_reply.started":"2026-07-06T10:50:05.682967Z","shell.execute_reply":"2026-07-06T10:50:59.162720Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ids = [\n    int(os.path.splitext(os.path.basename(f))[0])\n    for f in test_generator.filenames\n]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:59.164882Z","iopub.execute_input":"2026-07-06T10:50:59.165100Z","iopub.status.idle":"2026-07-06T10:50:59.170060Z","shell.execute_reply.started":"2026-07-06T10:50:59.165079Z","shell.execute_reply":"2026-07-06T10:50:59.169159Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = pd.DataFrame({\n    \"ID\": ids,\n    \"label\": predicted_labels\n})\n\nsubmission = submission.sort_values(\"ID\")\nsubmission.to_csv(\"submission.csv\", index=False)\nsubmission.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:59.171024Z","iopub.execute_input":"2026-07-06T10:50:59.171526Z","iopub.status.idle":"2026-07-06T10:50:59.223181Z","shell.execute_reply.started":"2026-07-06T10:50:59.171504Z","shell.execute_reply":"2026-07-06T10:50:59.222428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.dtypes\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:59.224017Z","iopub.execute_input":"2026-07-06T10:50:59.224293Z","iopub.status.idle":"2026-07-06T10:50:59.230155Z","shell.execute_reply.started":"2026-07-06T10:50:59.224265Z","shell.execute_reply":"2026-07-06T10:50:59.229503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# Grad-CAM visualization for the trained Xception model\n# Run this AFTER training (uses the in-memory `model`, `train_generator`,\n# `val_generator`, and IMG_SIZE defined above).\n# ============================================================\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.cm as cm\nfrom tensorflow.keras.applications.xception import preprocess_input\nfrom tensorflow.keras.utils import load_img, img_to_array\n\n# --- 1. Locate the last convolutional feature map of the Xception backbone ---\ndef find_last_conv_layer(m):\n    for name in [\"block14_sepconv2_act\"]:      # Xception's final conv activation\n        try:\n            m.get_layer(name)\n            return name\n        except Exception:\n            pass\n    for layer in reversed(m.layers):           # robust fallback: last 4D feature map\n        try:\n            if len(layer.output.shape) == 4:\n                return layer.name\n        except Exception:\n            pass\n    raise ValueError(\"No 4D convolutional layer found.\")\n\nlast_conv_layer_name = find_last_conv_layer(model)\nprint(\"Using last conv layer:\", last_conv_layer_name)\n\n# --- 2. Grad-CAM heatmap ---\ndef make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index=None):\n    grad_model = tf.keras.models.Model(\n        model.inputs,\n        [model.get_layer(last_conv_layer_name).output, model.output]\n    )\n    with tf.GradientTape() as tape:\n        conv_out, preds = grad_model(img_array)\n        if pred_index is None:\n            pred_index = tf.argmax(preds[0])\n        class_channel = preds[:, pred_index]\n    grads = tape.gradient(class_channel, conv_out)\n    pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))\n    conv_out = conv_out[0]\n    heatmap = conv_out @ pooled_grads[..., tf.newaxis]\n    heatmap = tf.squeeze(heatmap)\n    heatmap = tf.maximum(heatmap, 0) / (tf.reduce_max(heatmap) + tf.keras.backend.epsilon())\n    return heatmap.numpy(), int(pred_index)\n\n# --- 3. Pick a sample image (change the index to view a different one) ---\nclass_names = list(train_generator.class_indices.keys())\nsample_path = val_generator.filepaths[0]\n# To use your own file instead, just set: sample_path = \"/path/to/image.jpg\"\n\nraw_img   = load_img(sample_path, target_size=(IMG_SIZE, IMG_SIZE))\nimg       = img_to_array(raw_img)                                  # 0-255, for display\nimg_array = preprocess_input(np.expand_dims(img.copy(), axis=0))   # for the model\n\n# --- 4. Compute heatmap + prediction ---\nheatmap, pred_index = make_gradcam_heatmap(img_array, model, last_conv_layer_name)\npred_probs = model.predict(img_array, verbose=0)[0]\nprint(f\"Image: {sample_path}\")\nprint(f\"Predicted: {class_names[pred_index]}  ({pred_probs[pred_index]*100:.2f}%)\")\n\n# --- 5. Overlay heatmap on the original image ---\nheatmap_resized = tf.image.resize(heatmap[..., np.newaxis],\n                                  (IMG_SIZE, IMG_SIZE)).numpy().squeeze()\ncolored = cm.jet(heatmap_resized)[..., :3]\noverlay = np.clip(0.4 * colored + 0.6 * (img / 255.0), 0, 1)\n\nfig, ax = plt.subplots(1, 3, figsize=(14, 5))\nax[0].imshow(img.astype(\"uint8\"));        ax[0].set_title(\"Original\");            ax[0].axis(\"off\")\nax[1].imshow(heatmap_resized, cmap=\"jet\"); ax[1].set_title(\"Grad-CAM heatmap\");    ax[1].axis(\"off\")\nax[2].imshow(overlay);                     ax[2].set_title(f\"Overlay - {class_names[pred_index]}\"); ax[2].axis(\"off\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-06T10:50:59.231095Z","iopub.execute_input":"2026-07-06T10:50:59.231498Z","iopub.status.idle":"2026-07-06T10:51:11.837140Z","shell.execute_reply.started":"2026-07-06T10:50:59.231475Z","shell.execute_reply":"2026-07-06T10:51:11.836272Z"}},"outputs":[],"execution_count":null}]}