{"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":"none","dataSources":[{"sourceId":3362,"databundleVersionId":31148,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, re, zipfile, shutil, pathlib\nimport tensorflow as tf\nfrom tensorflow import keras\nfrom tensorflow.keras import layers\nimport numpy as np\nimport pandas as pd\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# --- CONFIG ---\nTRAIN_ZIP = '/kaggle/input/dogs-vs-cats/train.zip'\nTEST_ZIP  = '/kaggle/input/dogs-vs-cats/test1.zip'\nWORK_DIR  = '/kaggle/working/dogs-vs-cats'\nBATCH_SIZE = 32\nIMG_SIZE = (224,224)\nEPOCHS_HEAD = 8\nEPOCHS_FINE = 6\nAUTOTUNE = tf.data.AUTOTUNE\nMODEL_DIR = '/kaggle/working/model'\nos.makedirs(MODEL_DIR, exist_ok=True)\n\n# --- UNZIP safely ---\nos.makedirs(WORK_DIR, exist_ok=True)\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)\nif os.path.exists(TRAIN_ZIP):\n    safe_unzip(TRAIN_ZIP, WORK_DIR)\nelse:\n    print('Train zip not found at', TRAIN_ZIP)\nif os.path.exists(TEST_ZIP):\n    safe_unzip(TEST_ZIP, WORK_DIR)\nelse:\n    print('Test zip not found at', TEST_ZIP)\n\n# --- Locate train images (some zips contain a 'train' folder or images directly) ---\n# After unzipping Kaggle common structure: WORK_DIR/train/.jpg  and WORK_DIR/test1/.jpg\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' for p in pathlib.Path(d).glob('*.jpg')):\n        train_found = d\n        break\n\nif train_found is None:\n    # maybe images are in WORK_DIR/train/*.jpg in nested folder\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. Looked in: ' + str(possible_train_dirs))\nprint('Train images found in:', train_found)\n\n# --- Prepare structured folder: data/train/cat and data/train/dog ---\ndata_dir = '/kaggle/working/data'\ntrain_dir = os.path.join(data_dir, 'train')\nos.makedirs(train_dir, exist_ok=True)\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\n# Move (or copy) images into class folders based on filename prefix (cat. / dog.)\nmoved = 0\nfor p in pathlib.Path(train_found).glob('*.jpg'):\n    name = p.name.lower()\n    if name.startswith('cat'):\n        target = os.path.join(cat_dir, p.name)\n    elif name.startswith('dog'):\n        target = os.path.join(dog_dir, p.name)\n    else:\n        # fallback: try to parse 'cat' or 'dog' anywhere in filename\n        if 'cat' in name:\n            target = os.path.join(cat_dir, p.name)\n        elif 'dog' in name:\n            target = os.path.join(dog_dir, p.name)\n        else:\n            continue\n    # Use copy to avoid modifying input location\n    shutil.copy(str(p), target)\n    moved += 1\nprint('Copied', moved, 'train images into', cat_dir, 'and', dog_dir)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-03T14:51:59.290958Z","iopub.execute_input":"2025-12-03T14:51:59.291253Z","iopub.status.idle":"2025-12-03T14:52:11.756659Z","shell.execute_reply.started":"2025-12-03T14:51:59.291230Z","shell.execute_reply":"2025-12-03T14:52:11.755844Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Create training and validation datasets using image_dataset_from_directory ---\ntrain_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='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\n# Prefetch\ntrain_dataset = train_dataset.prefetch(AUTOTUNE)\nval_dataset = val_dataset.prefetch(AUTOTUNE)\n\n# --- Data augmentation layer (applied only in training) ---\ndata_augment = keras.Sequential([\n    layers.RandomFlip('horizontal'),\n    layers.RandomRotation(0.08),\n    layers.RandomZoom(0.06),\n])\n\n# --- Build model with EfficientNetB0 backbone ---\nbase_model = tf.keras.applications.EfficientNetB0(include_top=False, input_shape=(*IMG_SIZE,3), weights='imagenet')\nbase_model.trainable = False\n\ninputs = keras.Input(shape=(*IMG_SIZE,3))\nx = data_augment(inputs)\nx = tf.keras.applications.efficientnet.preprocess_input(x)  # use correct preprocessing\nx = base_model(x, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dropout(0.2)(x)\noutputs = layers.Dense(1, activation='sigmoid')(x)\nmodel = keras.Model(inputs, outputs)\n\nmodel.compile(optimizer=keras.optimizers.Adam(1e-3),\n              loss='binary_crossentropy',\n              metrics=['accuracy'])\nmodel.summary()\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T14:52:20.137635Z","iopub.execute_input":"2025-12-03T14:52:20.137896Z","iopub.status.idle":"2025-12-03T14:52:22.576674Z","shell.execute_reply.started":"2025-12-03T14:52:20.137878Z","shell.execute_reply":"2025-12-03T14:52:22.576051Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Callbacks ---\n# Note: best_model.keras is correct here\ncallbacks = [\n    keras.callbacks.ModelCheckpoint(\n        os.path.join(MODEL_DIR, 'best_model.keras'), \n        monitor='val_accuracy', \n        save_best_only=True, \n        verbose=1\n    ),\n    keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=2, verbose=1),\n    keras.callbacks.EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True)\n]\n\n# --- Train only the head first ---\nhistory = model.fit(train_dataset, validation_data=val_dataset, epochs=EPOCHS_HEAD, callbacks=callbacks)\n\n# --- Fine-tune: unfreeze top layers of base_model ---\nbase_model.trainable = True\n\n# Freeze most layers, fine-tune last ~50 layers\nfine_tune_at = len(base_model.layers) - 50\nfor layer in base_model.layers[:fine_tune_at]:\n    layer.trainable = False\n\n# Recompile with a low learning rate for fine-tuning\nmodel.compile(optimizer=keras.optimizers.Adam(1e-4),\n              loss='binary_crossentropy',\n              metrics=['accuracy'])\n\n# Train (Fine-tune)\nhistory_fine = model.fit(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# --- FIX 1: Add .keras extension here ---\nmodel.save(os.path.join(MODEL_DIR, 'final_model.keras'))\n\n# --- Prepare test predictions ---\ntest_folder = None\n# Check common Kaggle paths\npossible_paths = [\n    os.path.join(WORK_DIR, 'test1'), \n    os.path.join(WORK_DIR, 'test'), \n    os.path.join(WORK_DIR, 'test1', 'test1')\n]\n\nfor candidate in possible_paths:\n    if os.path.isdir(candidate):\n        test_folder = candidate\n        break\n\nif test_folder is None:\n    # Fallback search\n    for root, dirs, files in os.walk(WORK_DIR):\n        if any(f.lower().endswith('.jpg') for f in files):\n            if 'train' not in root.lower():\n                test_folder = root\n                break\n\nif test_folder:\n    # --- FIX 2: Numeric Sorting ---\n    # Standard sorted() treats '10.jpg' as coming before '2.jpg'. \n    # This lambda function tries to sort by the number inside the filename.\n    paths = pathlib.Path(test_folder).glob('*.jpg')\n    \n    def try_int_sort(p):\n        # Extracts numbers from filename for sorting (e.g. 10.jpg vs 2.jpg)\n        parts = re.split(r'(\\d+)', str(p))\n        return [int(part) if part.isdigit() else part for part in parts]\n\n    test_images = sorted([str(p) for p in paths], key=try_int_sort)\n    \n    print('Found', len(test_images), 'test images in', test_folder)\n\n    # Build dataset for prediction\n    def prep(path):\n        img = tf.io.read_file(path)\n        img = tf.image.decode_jpeg(img, channels=3)\n        img = tf.image.resize(img, IMG_SIZE)\n        img = tf.cast(img, tf.float32)\n        # Ensure efficientnet preprocess is what you want (it scales differently than ResNet)\n        img = tf.keras.applications.efficientnet.preprocess_input(img)\n        return img\n\n    test_ds = tf.data.Dataset.from_tensor_slices(test_images)\\\n        .map(prep, num_parallel_calls=tf.data.AUTOTUNE)\\\n        .batch(BATCH_SIZE)\\\n        .prefetch(tf.data.AUTOTUNE)\n\n    preds = model.predict(test_ds, verbose=1)\n\n    # Generate Submission\n    filenames = [pathlib.Path(p).name for p in test_images]\n    df = pd.DataFrame({\n        'id': [os.path.splitext(f)[0] for f in filenames],\n        'label': [float(p[0]) for p in preds]  # Probability of class 1\n    })\n\n    out_path = '/kaggle/working/submission.csv'\n    df.to_csv(out_path, index=False)\n    print('Wrote submission to', out_path)\n\nelse:\n    print('No test folder found; skip prediction.')\n\nprint('Done')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T14:52:54.194444Z","iopub.execute_input":"2025-12-03T14:52:54.195157Z","iopub.status.idle":"2025-12-03T15:09:35.728042Z","shell.execute_reply.started":"2025-12-03T14:52:54.195129Z","shell.execute_reply":"2025-12-03T15:09:35.727384Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix\nimport numpy as np\n\n# --- BAGIAN 1: Visualisasi Grafik Training (Loss & Accuracy) ---\n# Menggabungkan history dari tahap \"Head Training\" dan \"Fine Tuning\"\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\nplt.figure(figsize=(12, 6))\n\n# Plot Akurasi\nplt.subplot(1, 2, 1)\nplt.plot(acc, label='Training Accuracy')\nplt.plot(val_acc, label='Validation Accuracy')\nplt.plot([EPOCHS_HEAD-1, EPOCHS_HEAD-1], plt.ylim(), label='Start Fine Tuning', linestyle='--')\nplt.legend(loc='lower right')\nplt.title('Training and Validation Accuracy')\n\n# Plot Loss\nplt.subplot(1, 2, 2)\nplt.plot(loss, label='Training Loss')\nplt.plot(val_loss, label='Validation Loss')\nplt.plot([EPOCHS_HEAD-1, EPOCHS_HEAD-1], plt.ylim(), label='Start Fine Tuning', linestyle='--')\nplt.legend(loc='upper right')\nplt.title('Training and Validation Loss')\nplt.show()\n\n# --- BAGIAN 2: Evaluasi Detail pada Data Validasi ---\nprint(\"Sedang memproses prediksi pada data validasi...\")\n\n# Kita perlu mengambil label asli (y_true) dan prediksi (y_pred) dari dataset\ny_true = []\ny_pred_probs = []\n\n# Loop dataset validasi (batch demi batch)\nfor images, labels in val_dataset:\n    preds = model.predict(images, verbose=0)\n    y_pred_probs.extend(preds)\n    y_true.extend(labels.numpy())\n\ny_true = np.array(y_true)\ny_pred_probs = np.array(y_pred_probs).flatten() # Ubah ke 1D array\ny_pred_binary = (y_pred_probs > 0.5).astype(int) # Ambang batas 0.5\n\n# 1. Tampilkan Classification Report (Precision, Recall, F1-Score)\nprint(\"\\n--- Classification Report ---\")\nprint(classification_report(y_true, y_pred_binary, target_names=['Class 0', 'Class 1']))\n\n# 2. Tampilkan Confusion Matrix\ncm = confusion_matrix(y_true, y_pred_binary)\nplt.figure(figsize=(6, 5))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False)\nplt.xlabel('Predicted Label')\nplt.ylabel('True Label')\nplt.title('Confusion Matrix')\nplt.show()\n\n# --- BAGIAN 3: Visualisasi Hasil Prediksi (Gambar) ---\n# Menampilkan beberapa gambar sampel beserta prediksinya\ndef visualize_predictions(dataset, num_images=9):\n    plt.figure(figsize=(10, 10))\n    # Ambil 1 batch\n    for images, labels in dataset.take(1):\n        preds = model.predict(images, verbose=0)\n        for i in range(min(num_images, len(images))):\n            ax = plt.subplot(3, 3, i + 1)\n            img = images[i].numpy().astype(\"uint8\") # Pastikan tipe data gambar benar untuk ditampilkan\n            # Jika preprocessing menggunakan efisiennet, gambar mungkin perlu di-rescale balik agar warnanya normal\n            # Jika warnanya aneh, coba hilangkan komentar baris bawah:\n            # img = (img + 1) * 127.5 \n            \n            plt.imshow(img)\n            \n            prob = preds[i][0]\n            predicted_label = 1 if prob > 0.5 else 0\n            actual_label = int(labels[i])\n            \n            color = 'green' if predicted_label == actual_label else 'red'\n            \n            plt.title(f\"True: {actual_label} | Pred: {predicted_label}\\nConf: {prob:.2f}\", color=color)\n            plt.axis(\"off\")\n    plt.show()\n\nprint(\"\\n--- Contoh Hasil Prediksi Visual ---\")\nvisualize_predictions(val_dataset)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}