{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":5441,"databundleVersionId":38425,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ====================================================\n# Dogs vs. Cats Redux - Final Project Notebook (v3-style)\n# EfficientNetB0 + Fine-tuning + Label Smoothing + TTA\n# ====================================================\n\nimport os\nimport re\nimport zipfile\nimport shutil\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\n\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\n\nprint(\"TensorFlow version:\", tf.__version__)\n\n# ====================================================\n# 0. Paths and basic setup\n# ====================================================\n\nINPUT_DIR = \"/kaggle/input/dogs-vs-cats-redux-kernels-edition\"\nWORK_DIR = \"/kaggle/working\"\n\ntrain_zip_path = os.path.join(INPUT_DIR, \"train.zip\")\ntest_zip_path  = os.path.join(INPUT_DIR, \"test.zip\")\n\ntrain_raw = os.path.join(WORK_DIR, \"train_raw\")\ntest_raw  = os.path.join(WORK_DIR, \"test_raw\")\nTRAIN_DIR = os.path.join(WORK_DIR, \"train\")\nTEST_DIR  = os.path.join(WORK_DIR, \"test\")\n\n# Clean old folders if they exist (safe to re-run)\nfor d in [train_raw, test_raw, TRAIN_DIR, TEST_DIR]:\n    if os.path.exists(d):\n        shutil.rmtree(d)\n    os.makedirs(d, exist_ok=True)\n\n# ====================================================\n# 1. Unzip dataset into raw folders\n# ====================================================\n\nprint(\"Extracting train.zip...\")\nwith zipfile.ZipFile(train_zip_path, \"r\") as z:\n    z.extractall(train_raw)\n\nprint(\"Extracting test.zip...\")\nwith zipfile.ZipFile(test_zip_path, \"r\") as z:\n    z.extractall(test_raw)\n\n# ====================================================\n# 2. Flatten nested directories (important for this comp)\n# ====================================================\n\ndef flatten_folder(src_root, dst_root):\n    count = 0\n    for root, dirs, files in os.walk(src_root):\n        for file in files:\n            if file.lower().endswith(\".jpg\"):\n                src = os.path.join(root, file)\n                dst = os.path.join(dst_root, file)\n                shutil.move(src, dst)\n                count += 1\n    return count\n\ntrain_count = flatten_folder(train_raw, TRAIN_DIR)\ntest_count  = flatten_folder(test_raw,  TEST_DIR)\n\nprint(\"\\nFlattening complete.\")\nprint(\"Total train images:\", train_count)   # expected 25000\nprint(\"Total test images:\", test_count)     # expected 12500\nprint(\"Sample train files:\", os.listdir(TRAIN_DIR)[:5])\nprint(\"Sample test files:\", os.listdir(TEST_DIR)[:5])\n\n# ====================================================\n# 3. Build DataFrame for training\n# ====================================================\n\nfilepaths = []\nlabels = []\n\nfor fname in os.listdir(TRAIN_DIR):\n    if fname.lower().endswith(\".jpg\"):\n        label = 1 if fname.startswith(\"dog\") else 0  # dog=1, cat=0\n        filepaths.append(os.path.join(TRAIN_DIR, fname))\n        labels.append(label)\n\ntrain_df = pd.DataFrame({\"filepath\": filepaths, \"label\": labels})\nprint(\"\\nTrain DF size:\", len(train_df))\nprint(train_df[\"label\"].value_counts())\n\n# ====================================================\n# 4. Train/validation split\n# ====================================================\n\ntrain_df, val_df = train_test_split(\n    train_df,\n    test_size=0.2,\n    random_state=42,\n    stratify=train_df[\"label\"]\n)\n\nprint(\"Train size:\", len(train_df))\nprint(\"Val size:\", len(val_df))\n\n# ====================================================\n# 5. tf.data pipelines\n# ====================================================\n\nIMG_SIZE = 160       # EfficientNetB0 default resolution is 224, 160 is a good tradeoff\nBATCH_SIZE = 32\nAUTOTUNE = tf.data.AUTOTUNE\n\ndef decode_image(path, label=None):\n    \"\"\"Load + resize. Don't manually scale; EfficientNet preprocess_input will handle it.\"\"\"\n    image_bytes = tf.io.read_file(path)\n    image = tf.image.decode_jpeg(image_bytes, channels=3)\n    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))\n    image = tf.cast(image, tf.float32)      # still in [0, 255]\n    if label is not None:\n        return image, tf.cast(label, tf.float32)\n    else:\n        return image\n\ndata_augmentation = keras.Sequential(\n    [\n        layers.RandomFlip(\"horizontal\"),\n        layers.RandomRotation(0.08),\n        layers.RandomZoom(0.1),\n        layers.RandomContrast(0.1),\n    ],\n    name=\"data_augmentation\"\n)\n\ndef make_dataset(df, training=True):\n    paths  = df[\"filepath\"].values\n    labels = df[\"label\"].values\n\n    ds = tf.data.Dataset.from_tensor_slices((paths, labels))\n    if training:\n        ds = ds.shuffle(buffer_size=len(df), seed=42)\n\n    ds = ds.map(lambda x, y: decode_image(x, y), num_parallel_calls=AUTOTUNE)\n\n    if training:\n        ds = ds.map(lambda x, y: (data_augmentation(x, training=True), y),\n                    num_parallel_calls=AUTOTUNE)\n\n    return ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\ntrain_ds = make_dataset(train_df, training=True)\nval_ds   = make_dataset(val_df,   training=False)\n\n# ====================================================\n# 6. Build EfficientNetB0 model\n# ====================================================\n\nfrom tensorflow.keras.applications.efficientnet import EfficientNetB0, preprocess_input\n\nbase_model = EfficientNetB0(\n    include_top=False,\n    weights=\"imagenet\",\n    input_shape=(IMG_SIZE, IMG_SIZE, 3)\n)\nbase_model.trainable = False  # feature extractor first\n\ninputs = keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3))\nx = preprocess_input(inputs)          # scales from [0,255] -> [-1,1]\nx = base_model(x, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.4)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\n\nmodel = keras.Model(inputs, outputs, name=\"dogs_vs_cats_efficientnetb0\")\nmodel.summary()\n\n# ====================================================\n# 7. Initial training (feature extractor)\n# ====================================================\n\ninitial_epochs = 4   # short warmup\n\nloss_fn = keras.losses.BinaryCrossentropy(label_smoothing=0.05)\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-3),\n    loss=loss_fn,\n    metrics=[\"accuracy\", keras.metrics.AUC(name=\"AUC\")]\n)\n\ncallbacks_fe = [\n    keras.callbacks.EarlyStopping(\n        monitor=\"val_loss\",\n        patience=2,\n        restore_best_weights=True\n    ),\n    keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.3,\n        patience=1,\n        min_lr=1e-6\n    ),\n]\n\nhistory_fe = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=initial_epochs,\n    callbacks=callbacks_fe\n)\n\n# ====================================================\n# 8. Fine-tuning (unfreeze last part of EfficientNetB0)\n# ====================================================\n\nbase_model.trainable = True\n\n# Unfreeze last 30% of layers for fine-tuning\nfine_tune_at = int(len(base_model.layers) * 0.7)\nfor i, layer in enumerate(base_model.layers):\n    layer.trainable = (i >= fine_tune_at)\n\nfine_tune_epochs = 6\ntotal_epochs = initial_epochs + fine_tune_epochs\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-5),\n    loss=loss_fn,\n    metrics=[\"accuracy\", keras.metrics.AUC(name=\"AUC\")]\n)\n\ncallbacks_ft = [\n    keras.callbacks.EarlyStopping(\n        monitor=\"val_loss\",\n        patience=3,\n        restore_best_weights=True\n    ),\n    keras.callbacks.ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.3,\n        patience=1,\n        min_lr=1e-7\n    ),\n    keras.callbacks.ModelCheckpoint(\n        \"best_efficientnetb0_finetuned.keras\",\n        monitor=\"val_loss\",\n        save_best_only=True\n    ),\n]\n\nhistory_ft = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=total_epochs,\n    initial_epoch=len(history_fe.history[\"loss\"]),\n    callbacks=callbacks_ft\n)\n\n# ====================================================\n# 9. Plot training curves (for report and slides)\n# ====================================================\n\ndef merge_histories(h1, h2=None):\n    hist = {}\n    keys = set(list(h1.history.keys()) + ([] if not h2 else list(h2.history.keys())))\n    for k in keys:\n        hist[k] = []\n        if k in h1.history:\n            hist[k] += h1.history[k]\n        if h2 and k in h2.history:\n            hist[k] += h2.history[k]\n    return hist\n\nhist_all = merge_histories(history_fe, history_ft)\nepochs_range = range(1, len(hist_all[\"loss\"]) + 1)\n\nplt.figure(figsize=(8,5))\nplt.plot(epochs_range, hist_all[\"loss\"], label=\"Train loss\")\nplt.plot(epochs_range, hist_all[\"val_loss\"], label=\"Val loss\")\nplt.xlabel(\"Epochs\"); plt.ylabel(\"Loss\")\nplt.title(\"Training and validation loss\")\nplt.legend(); plt.grid(True); plt.show()\n\nplt.figure(figsize=(8,5))\nplt.plot(epochs_range, hist_all[\"accuracy\"], label=\"Train accuracy\")\nplt.plot(epochs_range, hist_all[\"val_accuracy\"], label=\"Val accuracy\")\nplt.xlabel(\"Epochs\"); plt.ylabel(\"Accuracy\")\nplt.title(\"Training and validation accuracy\")\nplt.legend(); plt.grid(True); plt.show()\n\n# ====================================================\n# 10. Evaluate on validation set (for reporting)\n# ====================================================\n\nval_loss, val_acc, val_auc = model.evaluate(val_ds)\nprint(\"FINAL VAL LOSS:\", val_loss)\nprint(\"FINAL VAL ACC :\", val_acc)\nprint(\"FINAL VAL AUC :\", val_auc)\n\n# ====================================================\n# 11. Test-Time Augmentation (TTA) + Submission\n# ====================================================\n\n# Only .jpg files in test directory\ntest_files = sorted(\n    [f for f in os.listdir(TEST_DIR) if f.lower().endswith(\".jpg\")]\n)\n\ntest_ids   = [int(os.path.splitext(f)[0]) for f in test_files]\ntest_paths = [os.path.join(TEST_DIR, f) for f in test_files]\n\n# Base test dataset (no augmentations yet)\nraw_test_ds = tf.data.Dataset.from_tensor_slices(test_paths)\nraw_test_ds = raw_test_ds.map(\n    lambda x: decode_image(x, None),\n    num_parallel_calls=AUTOTUNE\n).cache()\n\ntest_ds = raw_test_ds.batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\n# Simple horizontal-flip TTA\ntta_flip_ds = raw_test_ds.map(\n    lambda x: tf.image.flip_left_right(x),\n    num_parallel_calls=AUTOTUNE\n).batch(BATCH_SIZE).prefetch(AUTOTUNE)\n\nprint(\"Predicting on original test images...\")\npreds_orig = model.predict(test_ds, verbose=1).reshape(-1)\n\nprint(\"Predicting on horizontally flipped test images...\")\npreds_flip = model.predict(tta_flip_ds, verbose=1).reshape(-1)\n\n# Average the predictions\npreds = (preds_orig + preds_flip) / 2.0\n\nsubmission = pd.DataFrame({\"id\": test_ids, \"label\": preds})\nsubmission = submission.sort_values(\"id\")\nsubmission.to_csv(\"submission.csv\", index=False)\n\nprint(submission.head())\nprint(\"Saved submission.csv\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-25T06:37:40.018187Z","iopub.execute_input":"2025-11-25T06:37:40.018959Z"}},"outputs":[],"execution_count":null}]}