{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":3362}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Dogs vs Cats — Transfer Learning (VGG16)\n\nเวอร์ชันแก้ไขสำหรับงาน:\n- Validation accuracy เป้าหมาย **≥ 0.97**\n- Predict รูปจาก `test1` อย่างน้อย **10 รูป**\n- ใช้ VGG16 + fine-tuning\n- แยก augmentation ของ train ออกจาก validation/test เพื่อไม่ให้ผลประเมินเพี้ยน\n","metadata":{}},{"cell_type":"code","source":"# Kaggle paths\nimport os\nimport zipfile\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\n\nprint(\"TensorFlow:\", tf.__version__)\nprint(\"GPU:\", tf.config.list_physical_devices(\"GPU\"))\n\nDATA_PATH = \"/kaggle/input/competitions/dogs-vs-cats\"\nTRAIN_ZIP = f\"{DATA_PATH}/train.zip\"\nTEST_ZIP = f\"{DATA_PATH}/test1.zip\"\n\n# Extract only if the folders do not already exist\nif not os.path.isdir(\"train\"):\n    with zipfile.ZipFile(TRAIN_ZIP, \"r\") as z:\n        z.extractall(\".\")\nif not os.path.isdir(\"test1\"):\n    with zipfile.ZipFile(TEST_ZIP, \"r\") as z:\n        z.extractall(\".\")\n\nprint(\"train:\", len(os.listdir(\"train\")))\nprint(\"test1:\", len(os.listdir(\"test1\")))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T17:43:15.005826Z","iopub.execute_input":"2026-09-12T17:43:15.006162Z","iopub.status.idle":"2026-09-12T17:43:16.013196Z","shell.execute_reply.started":"2026-09-12T17:43:15.006133Z","shell.execute_reply":"2026-09-12T17:43:16.012304Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1) แบ่ง train/validation\n\nใช้ validation 25% จาก `train.zip` ตามแนวทางเดิม แต่สร้างโฟลเดอร์แยกใหม่อย่างชัดเจน","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nfrom shutil import copy2\nimport random\n\nSEED = 1337\nVAL_RATIO = 0.25\n\ndataset_home = Path(\"dataset_dogs_vs_cats\")\ntrain_dir = dataset_home / \"train\"\nval_dir = dataset_home / \"val\"\n\n# Create folders\nfor d in [train_dir / \"cats\", train_dir / \"dogs\",\n          val_dir / \"cats\", val_dir / \"dogs\"]:\n    d.mkdir(parents=True, exist_ok=True)\n\n# Split once; skip copying if both classes already contain files.\nexisting = sum(len(list((train_dir / c).glob(\"*.jpg\"))) for c in [\"cats\",\"dogs\"])\nif existing == 0:\n    random.seed(SEED)\n    files = list(Path(\"train\").glob(\"*.jpg\"))\n    random.shuffle(files)\n\n    for f in files:\n        cls = \"cat\" if f.name.startswith(\"cat.\") else \"dog\"\n        target_root = val_dir if random.random() < VAL_RATIO else train_dir\n        copy2(f, target_root / ((\"cats\" if cls == \"cat\" else \"dogs\")) / f.name)\n\nprint(\"Train cats:\", len(list((train_dir/\"cats\").glob(\"*.jpg\"))))\nprint(\"Train dogs:\", len(list((train_dir/\"dogs\").glob(\"*.jpg\"))))\nprint(\"Val cats:\", len(list((val_dir/\"cats\").glob(\"*.jpg\"))))\nprint(\"Val dogs:\", len(list((val_dir/\"dogs\").glob(\"*.jpg\"))))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T17:43:16.014605Z","iopub.execute_input":"2026-09-12T17:43:16.014910Z","iopub.status.idle":"2026-09-12T17:43:16.148007Z","shell.execute_reply.started":"2026-09-12T17:43:16.014887Z","shell.execute_reply":"2026-09-12T17:43:16.147106Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2) Data generators\n\n**จุดที่แก้สำคัญ:** validation ห้ามใช้ random augmentation เพราะจะทำให้คะแนน validation แกว่ง/ไม่สะท้อนข้อมูลจริง","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nIMAGE_SIZE = (224, 224)\nBATCH_SIZE = 32\n\n# Train: augmentation + VGG16 preprocessing\ntrain_datagen = ImageDataGenerator(\n    preprocessing_function=tf.keras.applications.vgg16.preprocess_input,\n    rotation_range=15,\n    width_shift_range=0.10,\n    height_shift_range=0.10,\n    zoom_range=0.15,\n    horizontal_flip=True\n)\n\n# Validation: preprocessing only\nval_datagen = ImageDataGenerator(\n    preprocessing_function=tf.keras.applications.vgg16.preprocess_input\n)\n\ntrain_it = train_datagen.flow_from_directory(\n    str(train_dir),\n    target_size=IMAGE_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode=\"binary\",\n    shuffle=True,\n    seed=SEED\n)\n\nval_it = val_datagen.flow_from_directory(\n    str(val_dir),\n    target_size=IMAGE_SIZE,\n    batch_size=BATCH_SIZE,\n    class_mode=\"binary\",\n    shuffle=False\n)\n\nprint(\"Class mapping:\", train_it.class_indices)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T17:43:16.149127Z","iopub.execute_input":"2026-09-12T17:43:16.150018Z","iopub.status.idle":"2026-09-12T17:43:16.843805Z","shell.execute_reply.started":"2026-09-12T17:43:16.149981Z","shell.execute_reply":"2026-09-12T17:43:16.843030Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3) VGG16 — Transfer Learning\n\nเริ่มจาก freeze VGG16 ก่อน แล้วฝึกเฉพาะ classifier","metadata":{}},{"cell_type":"code","source":"from tensorflow.keras.applications import VGG16\nfrom tensorflow.keras import layers, Model\n\nbase_model = VGG16(\n    weights=\"imagenet\",\n    include_top=False,\n    input_shape=(224, 224, 3)\n)\n\nbase_model.trainable = False\n\ninputs = layers.Input(shape=(224, 224, 3))\nx = base_model(inputs, training=False)\nx = layers.GlobalAveragePooling2D()(x)\nx = layers.Dense(256, activation=\"relu\")(x)\nx = layers.Dropout(0.5)(x)\noutputs = layers.Dense(1, activation=\"sigmoid\")(x)\n\nmodel = Model(inputs, outputs)\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nmodel.summary()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T17:43:16.845281Z","iopub.execute_input":"2026-09-12T17:43:16.845499Z","iopub.status.idle":"2026-09-12T17:43:19.325803Z","shell.execute_reply.started":"2026-09-12T17:43:16.845479Z","shell.execute_reply":"2026-09-12T17:43:19.325171Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\n\ncallbacks = [\n    ModelCheckpoint(\n        \"best_vgg16.keras\",\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.2,\n        patience=2,\n        min_lr=1e-7,\n        verbose=1\n    ),\n    EarlyStopping(\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        patience=4,\n        restore_best_weights=True,\n        verbose=1\n    )\n]\n\nhistory_1 = model.fit(\n    train_it,\n    validation_data=val_it,\n    epochs=8,\n    callbacks=callbacks,\n    verbose=1\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T17:43:19.326664Z","iopub.execute_input":"2026-09-12T17:43:19.326864Z","iopub.status.idle":"2026-09-12T18:18:28.182846Z","shell.execute_reply.started":"2026-09-12T17:43:19.326844Z","shell.execute_reply":"2026-09-12T18:18:28.182119Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4) Fine-tuning\n","metadata":{}},{"cell_type":"code","source":"# Unfreeze only the last VGG16 convolution block (block5)\nbase_model.trainable = True\n\nfor layer in base_model.layers:\n    layer.trainable = layer.name.startswith(\"block5\")\n\n# Keep BatchNorm layers frozen if present\nfor layer in base_model.layers:\n    if isinstance(layer, layers.BatchNormalization):\n        layer.trainable = False\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),\n    loss=\"binary_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\nfine_tune_callbacks = [\n    ModelCheckpoint(\n        \"best_vgg16_finetuned.keras\",\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        save_best_only=True,\n        verbose=1\n    ),\n    ReduceLROnPlateau(\n        monitor=\"val_loss\",\n        factor=0.2,\n        patience=2,\n        min_lr=1e-8,\n        verbose=1\n    ),\n    EarlyStopping(\n        monitor=\"val_accuracy\",\n        mode=\"max\",\n        patience=4,\n        restore_best_weights=True,\n        verbose=1\n    )\n]\n\nhistory_2 = model.fit(\n    train_it,\n    validation_data=val_it,\n    epochs=5,\n    callbacks=fine_tune_callbacks,\n    verbose=1\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:20:43.285130Z","iopub.execute_input":"2026-09-12T18:20:43.285781Z","iopub.status.idle":"2026-09-12T18:42:34.659618Z","shell.execute_reply.started":"2026-09-12T18:20:43.285751Z","shell.execute_reply":"2026-09-12T18:42:34.658870Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5) ตรวจ Validation Accuracy\n\nต้องดูค่า `val_accuracy` สูงสุด ไม่ใช่ดู training accuracy อย่างเดียว","metadata":{}},{"cell_type":"code","source":"# Load the best checkpoint if it exists\nbest_path = \"best_vgg16_finetuned.keras\"\nif os.path.exists(best_path):\n    model = tf.keras.models.load_model(best_path)\n\nval_loss, val_acc = model.evaluate(val_it, verbose=1)\n\nprint(f\"Validation accuracy = {val_acc:.4f} ({val_acc*100:.2f}%)\")\nif val_acc >= 0.97:\n    print(\"PASS: Validation accuracy >= 0.97\")\nelse:\n    print(\"ยังไม่ถึง 0.97 — ให้รัน fine-tuning ต่อ/เพิ่ม epochs หรือปรับ augmentation\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:44:04.469773Z","iopub.execute_input":"2026-09-12T18:44:04.470572Z","iopub.status.idle":"2026-09-12T18:44:38.148876Z","shell.execute_reply.started":"2026-09-12T18:44:04.470495Z","shell.execute_reply":"2026-09-12T18:44:38.148195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef plot_history(h1, h2=None):\n    acc = list(h1.history.get(\"accuracy\", []))\n    val_acc = list(h1.history.get(\"val_accuracy\", []))\n    loss = list(h1.history.get(\"loss\", []))\n    val_loss = list(h1.history.get(\"val_loss\", []))\n\n    if h2 is not None:\n        acc += h2.history.get(\"accuracy\", [])\n        val_acc += h2.history.get(\"val_accuracy\", [])\n        loss += h2.history.get(\"loss\", [])\n        val_loss += h2.history.get(\"val_loss\", [])\n\n    plt.figure(figsize=(10,4))\n    plt.plot(acc, label=\"train accuracy\")\n    plt.plot(val_acc, label=\"validation accuracy\")\n    plt.axhline(0.97, linestyle=\"--\", label=\"target 0.97\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Accuracy\")\n    plt.legend()\n    plt.show()\n\n    plt.figure(figsize=(10,4))\n    plt.plot(loss, label=\"train loss\")\n    plt.plot(val_loss, label=\"validation loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.legend()\n    plt.show()\n\nplot_history(history_1) #history_2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:47:15.341849Z","iopub.execute_input":"2026-09-12T18:47:15.342712Z","iopub.status.idle":"2026-09-12T18:47:15.608607Z","shell.execute_reply.started":"2026-09-12T18:47:15.342679Z","shell.execute_reply":"2026-09-12T18:47:15.607753Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6) Prepare `test1` สำหรับ prediction\n\n**ไม่ใช้ horizontal flip หรือ augmentation ตอน predict** และ `shuffle=False` เพื่อให้ลำดับไฟล์ตรงกับผล prediction","metadata":{}},{"cell_type":"code","source":"from glob import glob\n\ntest_files = sorted(glob(\"/kaggle/working/test1/*.jpg\"))\ntest_df = pd.DataFrame({\"filename\": test_files})\n\ntest_datagen = ImageDataGenerator(\n    preprocessing_function=tf.keras.applications.vgg16.preprocess_input\n)\n\ntest_it = test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    x_col=\"filename\",\n    y_col=None,\n    target_size=IMAGE_SIZE,\n    batch_size=32,\n    class_mode=None,\n    shuffle=False\n)\n\nprint(\"Test images:\", len(test_df))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:47:19.023738Z","iopub.execute_input":"2026-09-12T18:47:19.024629Z","iopub.status.idle":"2026-09-12T18:47:19.151935Z","shell.execute_reply.started":"2026-09-12T18:47:19.024587Z","shell.execute_reply":"2026-09-12T18:47:19.151109Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7) Predict อย่างน้อย 10 รูป","metadata":{}},{"cell_type":"code","source":"# Predict the first 10 test images\nN = 10\n\ntest_it.reset()\npred_prob = model.predict(test_it, verbose=1).reshape(-1)\n\npred_label = np.where(pred_prob >= 0.5, \"dog\", \"cat\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:53:01.009501Z","iopub.execute_input":"2026-09-12T18:53:01.010045Z","iopub.status.idle":"2026-09-12T18:54:27.267015Z","shell.execute_reply.started":"2026-09-12T18:53:01.010015Z","shell.execute_reply":"2026-09-12T18:54:27.266195Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom tensorflow.keras.utils import load_img\n\nN = 10\n\nfig, axes = plt.subplots(2, 5, figsize=(18, 8))\naxes = axes.ravel()\n\nfor i in range(N):\n    img = load_img(\n        test_df[\"filename\"].iloc[i],\n        target_size=IMAGE_SIZE\n    )\n\n    # แปลงค่าความน่าจะเป็นเป็น cat / dog\n    label = \"dog\" if pred_prob[i] >= 0.5 else \"cat\"\n\n    axes[i].imshow(img)\n    axes[i].set_title(\n        f\"{label} | dog={pred_prob[i]:.3f}\"\n    )\n    axes[i].axis(\"off\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-12T18:55:42.907811Z","iopub.execute_input":"2026-09-12T18:55:42.908481Z","iopub.status.idle":"2026-09-12T18:55:44.068636Z","shell.execute_reply.started":"2026-09-12T18:55:42.908450Z","shell.execute_reply":"2026-09-12T18:55:44.065003Z"}},"outputs":[],"execution_count":null}]}