{"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":3362,"databundleVersionId":31148,"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 - IMAGE CLASSIFICATION (TF/KERAS)\n# Transfer Learning (EfficientNetB0) + Charts\n# ============================================\n\nimport os, zipfile, shutil, pathlib\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport numpy as np\nfrom sklearn.metrics import confusion_matrix, classification_report\nimport matplotlib.pyplot as plt\n\n# =========================\n# CONFIG\n# =========================\nTRAIN_ZIP   = '/kaggle/input/dogs-vs-cats/train.zip'\nWORK_DIR    = '/kaggle/working/dogs-vs-cats'\nDATA_DIR    = '/kaggle/working/data'\nMODEL_DIR   = '/kaggle/working/model'\n\nBATCH_SIZE       = 32\nIMG_SIZE         = (224, 224)\nEPOCHS_HEAD      = 5      # train \"head\" saja (backbone freeze)\nEPOCHS_FINE      = 5      # fine-tuning backbone\nAUTOTUNE         = tf.data.AUTOTUNE\nMAX_PER_CLASS    = 4000   # pakai sebagian data (maksimal per kelas)\n\nos.makedirs(MODEL_DIR, exist_ok=True)\nos.makedirs(WORK_DIR, exist_ok=True)\n\n# =========================\n# UNZIP TRAIN DATA\n# =========================\ndef safe_unzip(zip_path, extract_to):\n    print('Unzipping', zip_path, '->', extract_to)\n    with zipfile.ZipFile(zip_path, 'r') as z:\n        z.extractall(extract_to)\n\nif os.path.exists(TRAIN_ZIP):\n    safe_unzip(TRAIN_ZIP, WORK_DIR)\nelse:\n    raise FileNotFoundError('Train zip not found at ' + TRAIN_ZIP)\n\n# =========================\n# LOCATE TRAIN IMAGES\n# =========================\npossible_train_dirs = [\n    os.path.join(WORK_DIR, 'train'),\n    WORK_DIR\n]\ntrain_found = None\nfor d in possible_train_dirs:\n    if os.path.isdir(d) and any(p.suffix.lower() == '.jpg'\n                                for p in pathlib.Path(d).glob('*.jpg')):\n        train_found = d\n        break\n\nif train_found is None:\n    # fallback: walk all folders\n    for root, dirs, files in os.walk(WORK_DIR):\n        if any(f.lower().endswith('.jpg') for f in files):\n            train_found = root\n            break\n\nif train_found is None:\n    raise FileNotFoundError('Could not find train images after unzipping.')\nprint('Train images found in:', train_found)\n\n# =========================\n# BUILD FOLDER STRUCTURE:\n# data/train/cat & data/train/dog\n# =========================\ntrain_dir = os.path.join(DATA_DIR, 'train')\ncat_dir   = os.path.join(train_dir, 'cat')\ndog_dir   = os.path.join(train_dir, 'dog')\nos.makedirs(cat_dir, exist_ok=True)\nos.makedirs(dog_dir, exist_ok=True)\n\nper_class_count = {'cat': 0, 'dog': 0}\n\n# Ambil hanya sebagian gambar (MAX_PER_CLASS per kelas)\nfor p in pathlib.Path(train_found).glob('*.jpg'):\n    name = p.name.lower()\n\n    label = None\n    if name.startswith('cat'):\n        label = 'cat'\n    elif name.startswith('dog'):\n        label = 'dog'\n    else:\n        if 'cat' in name:\n            label = 'cat'\n        elif 'dog' in name:\n            label = 'dog'\n\n    if label is None:\n        continue\n\n    # cek limit per kelas\n    if per_class_count[label] >= MAX_PER_CLASS:\n        continue\n\n    target_dir = cat_dir if label == 'cat' else dog_dir\n    shutil.copy(str(p), os.path.join(target_dir, p.name))\n    per_class_count[label] += 1\n\nprint('Copied images per class:', per_class_count)\ntotal_used = per_class_count['cat'] + per_class_count['dog']\nprint('Total images used:', total_used)\n\n# =========================\n# CREATE TRAIN / VAL DATASETS\n# =========================\ntrain_dataset = tf.keras.preprocessing.image_dataset_from_directory(\n    train_dir,\n    labels='inferred',\n    label_mode='binary',      # 0 = cat, 1 = dog\n    validation_split=0.2,\n    subset='training',\n    seed=123,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE\n)\nval_dataset = tf.keras.preprocessing.image_dataset_from_directory(\n    train_dir,\n    labels='inferred',\n    label_mode='binary',\n    validation_split=0.2,\n    subset='validation',\n    seed=123,\n    image_size=IMG_SIZE,\n    batch_size=BATCH_SIZE\n)\n\ntrain_dataset = train_dataset.prefetch(AUTOTUNE)\nval_dataset   = val_dataset.prefetch(AUTOTUNE)\n\n# =========================\n# DATA AUGMENTATION\n# =========================\ndata_augment = keras.Sequential(\n    [\n        layers.RandomFlip('horizontal'),\n        layers.RandomRotation(0.08),\n        layers.RandomZoom(0.06),\n    ],\n    name='data_augmentation'\n)\n\n# =========================\n# MODEL: EfficientNetB0 (Transfer Learning)\n# Konsep: pretrained backbone + FC layer baru\n# =========================\nbase_model = tf.keras.applications.EfficientNetB0(\n    include_top=False,\n    input_shape=(*IMG_SIZE, 3),\n    weights='imagenet'\n)\nbase_model.trainable = False  # tahap pertama: freeze backbone\n\ninputs = keras.Input(shape=(*IMG_SIZE, 3), name='input_image')\nx = data_augment(inputs)\nx = tf.keras.applications.efficientnet.preprocess_input(x)\nx = base_model(x, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.2)(x)\noutputs = layers.Dense(1, activation='sigmoid', name='prediction')(x)\nmodel = keras.Model(inputs, outputs, name='dogs_vs_cats_efficientnet')\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-3),\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nmodel.summary()\n\n# =========================\n# CALLBACKS\n# =========================\nbest_model_path = os.path.join(MODEL_DIR, 'best_model.keras')\n\ncallbacks = [\n    keras.callbacks.ModelCheckpoint(\n        best_model_path,\n        monitor='val_accuracy',\n        save_best_only=True,\n        verbose=1\n    ),\n    keras.callbacks.ReduceLROnPlateau(\n        monitor='val_loss',\n        factor=0.5,\n        patience=2,\n        verbose=1\n    ),\n    keras.callbacks.EarlyStopping(\n        monitor='val_loss',\n        patience=4,\n        restore_best_weights=True,\n        verbose=1\n    )\n]\n\n# =========================\n# STEP 1: TRAIN HEAD (BACKBONE FROZEN)\n# =========================\nhistory = model.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=EPOCHS_HEAD,\n    callbacks=callbacks\n)\n\n# =========================\n# STEP 2: FINE-TUNE BACKBONE (UNFREEZE BAGIAN ATAS)\n# =========================\nbase_model.trainable = True\n\n# Hanya buka beberapa layer terakhir supaya tidak terlalu berat\nfine_tune_at = len(base_model.layers) - 50\nfor layer in base_model.layers[:fine_tune_at]:\n    layer.trainable = False\n\nmodel.compile(\n    optimizer=keras.optimizers.Adam(1e-4),\n    loss='binary_crossentropy',\n    metrics=['accuracy']\n)\n\nhistory_fine = model.fit(\n    train_dataset,\n    validation_data=val_dataset,\n    epochs=EPOCHS_HEAD + EPOCHS_FINE,\n    initial_epoch=history.epoch[-1] + 1,\n    callbacks=callbacks\n)\n\n# =========================\n# SAVE FINAL MODEL (.keras)\n# =========================\nfinal_path = os.path.join(MODEL_DIR, 'final_model.keras')\nmodel.save(final_path)\nprint('Training done. Final model saved at:', final_path)\nprint('Best checkpoint saved at:', best_model_path)\n\n# ======================================\n# PLOTTING: TRAIN vs VAL ACCURACY/LOSS\n# ======================================\n\n# gabungkan history head + fine-tune\nacc      = history.history['accuracy']      + history_fine.history['accuracy']\nval_acc  = history.history['val_accuracy']  + history_fine.history['val_accuracy']\nloss     = history.history['loss']          + history_fine.history['loss']\nval_loss = history.history['val_loss']      + history_fine.history['val_loss']\n\nepochs_range = range(1, len(acc) + 1)\n\n# Accuracy plot\nplt.figure(figsize=(6, 4))\nplt.plot(epochs_range, acc, label='Train Accuracy')\nplt.plot(epochs_range, val_acc, label='Val Accuracy')\nplt.xlabel('Epoch')\nplt.ylabel('Accuracy')\nplt.title('Training and Validation Accuracy')\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# Loss plot\nplt.figure(figsize=(6, 4))\nplt.plot(epochs_range, loss, label='Train Loss')\nplt.plot(epochs_range, val_loss, label='Val Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.title('Training and Validation Loss')\nplt.legend()\nplt.grid(True)\nplt.show()\n\n# ======================================\n# CONFUSION MATRIX & CLASSIFICATION REPORT\n# ======================================\n\ny_true = []\ny_pred = []\n\nfor batch_images, batch_labels in val_dataset:\n    preds = model.predict(batch_images, verbose=0)\n    y_true.extend(batch_labels.numpy().ravel())\n    y_pred.extend((preds.ravel() > 0.5).astype(int))  # threshold 0.5\n\ny_true = np.array(y_true, dtype=int)\ny_pred = np.array(y_pred, dtype=int)\n\ncm = confusion_matrix(y_true, y_pred)\nprint(\"Confusion matrix:\\n\", cm)\n\nprint(\"\\nClassification report:\")\nprint(classification_report(y_true, y_pred, target_names=['cat', 'dog']))\n\n# Plot confusion matrix sederhana\nplt.figure(figsize=(4, 4))\nplt.imshow(cm)\nplt.title('Confusion Matrix')\nplt.xlabel('Predicted')\nplt.ylabel('Actual')\nplt.xticks([0, 1], ['cat', 'dog'])\nplt.yticks([0, 1], ['cat', 'dog'])\n\nfor i in range(2):\n    for j in range(2):\n        plt.text(j, i, cm[i, j], ha='center', va='center')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-06T03:42:37.128995Z","iopub.execute_input":"2025-12-06T03:42:37.129387Z","iopub.status.idle":"2025-12-06T03:46:17.10571Z","shell.execute_reply.started":"2025-12-06T03:42:37.129354Z","shell.execute_reply":"2025-12-06T03:46:17.104834Z"}},"outputs":[],"execution_count":null}]}