{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"7d85fd68","cell_type":"markdown","source":"# Dogs vs Cats — Transfer Learning & Fine-Tuning\n\nฝึกใช้ Pre-trained Models อย่างน้อย 3 โมเดล และประเมินผลด้วย Classification Report จาก test set\n\nModels:\n- VGG16\n- InceptionV3\n- NASNetLarge\n\nทุกโมเดลใช้ ImageNet pretrained weights และมีขั้นตอน Fine-Tuning","metadata":{}},{"id":"6f676dc9","cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nimport matplotlib.pyplot as plt\n\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score\n\nfrom tensorflow.keras import Model\nfrom tensorflow.keras.layers import Dense, Dropout, GlobalAveragePooling2D\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\n\nfrom tensorflow.keras.applications import VGG16, InceptionV3, NASNetLarge\nfrom tensorflow.keras.applications.vgg16 import preprocess_input as vgg16_preprocess\nfrom tensorflow.keras.applications.inception_v3 import preprocess_input as inception_preprocess\nfrom tensorflow.keras.applications.nasnet import preprocess_input as nasnet_preprocess\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nprint(\"TensorFlow:\", tf.__version__)\nprint(\"GPU:\", tf.config.list_physical_devices(\"GPU\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:27:41.122652Z","iopub.execute_input":"2026-10-02T20:27:41.123218Z","iopub.status.idle":"2026-10-02T20:27:41.501321Z","shell.execute_reply.started":"2026-10-02T20:27:41.123185Z","shell.execute_reply":"2026-10-02T20:27:41.500416Z"}},"outputs":[],"execution_count":null},{"id":"cbbfdae4","cell_type":"markdown","source":"## 1. Dataset\n\nใช้ dataset ที่มีโฟลเดอร์ `train/cats`, `train/dogs`, `test/cats`, `test/dogs`\n\nค่าเริ่มต้นใช้ข้อมูล train ทั้งชุด เพื่อไม่ลดข้อมูลสำหรับการฝึกโมเดล","metadata":{}},{"id":"47b7574a","cell_type":"code","source":"CANDIDATE_ROOTS = [\n    \"/kaggle/input/datasets/kasikrit/308-456-dogs-vs-cats-dataset/dataset_dogs_vs_cats\",\n    \"/kaggle/working/dataset_dogs_vs_cats\",\n    \"dataset_dogs_vs_cats\",\n]\n\nDATA_ROOT = next(\n    (\n        root for root in CANDIDATE_ROOTS\n        if os.path.isdir(os.path.join(root, \"train\", \"cats\"))\n        and os.path.isdir(os.path.join(root, \"train\", \"dogs\"))\n        and os.path.isdir(os.path.join(root, \"test\", \"cats\"))\n        and os.path.isdir(os.path.join(root, \"test\", \"dogs\"))\n    ),\n    None,\n)\n\nif DATA_ROOT is None:\n    raise FileNotFoundError(\n        \"ไม่พบ dataset ที่มี train/cats, train/dogs, test/cats และ test/dogs\"\n    )\n\nTRAIN_DIR = os.path.join(DATA_ROOT, \"train\")\nTEST_DIR = os.path.join(DATA_ROOT, \"test\")\nCLASS_NAMES = [\"cats\", \"dogs\"]\n\nprint(\"DATA_ROOT:\", DATA_ROOT)\nprint(\"TRAIN_DIR:\", TRAIN_DIR)\nprint(\"TEST_DIR :\", TEST_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:27:41.502947Z","iopub.execute_input":"2026-10-02T20:27:41.503304Z","iopub.status.idle":"2026-10-02T20:27:41.514072Z","shell.execute_reply.started":"2026-10-02T20:27:41.50328Z","shell.execute_reply":"2026-10-02T20:27:41.513119Z"}},"outputs":[],"execution_count":null},{"id":"03062c50","cell_type":"code","source":"def count_images(root):\n    result = {}\n    for cls in CLASS_NAMES:\n        folder = os.path.join(root, cls)\n        result[cls] = sum(\n            os.path.isfile(os.path.join(folder, f))\n            for f in os.listdir(folder)\n        )\n    return result\n\nprint(\"Train:\", count_images(TRAIN_DIR))\nprint(\"Test :\", count_images(TEST_DIR))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:27:41.515197Z","iopub.execute_input":"2026-10-02T20:27:41.515649Z","iopub.status.idle":"2026-10-02T20:28:02.659423Z","shell.execute_reply.started":"2026-10-02T20:27:41.515627Z","shell.execute_reply":"2026-10-02T20:28:02.658514Z"}},"outputs":[],"execution_count":null},{"id":"162402ba","cell_type":"markdown","source":"## 2. Generator\n\nแยก generator ตาม preprocessing ของแต่ละ pretrained model  \nvalidation และ test ไม่ทำ augmentation","metadata":{}},{"id":"b9f3285c","cell_type":"code","source":"def make_generators(image_size, preprocess_fn, batch_size=32, val_split=0.2):\n    train_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_fn,\n        horizontal_flip=True,\n        rotation_range=10,\n        width_shift_range=0.10,\n        height_shift_range=0.10,\n        zoom_range=0.10,\n        validation_split=val_split,\n    )\n\n    val_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_fn,\n        validation_split=val_split,\n    )\n\n    test_datagen = ImageDataGenerator(\n        preprocessing_function=preprocess_fn,\n    )\n\n    train_it = train_datagen.flow_from_directory(\n        TRAIN_DIR,\n        classes=CLASS_NAMES,\n        class_mode=\"binary\",\n        target_size=(image_size, image_size),\n        batch_size=batch_size,\n        subset=\"training\",\n        shuffle=True,\n        seed=SEED,\n    )\n\n    val_it = val_datagen.flow_from_directory(\n        TRAIN_DIR,\n        classes=CLASS_NAMES,\n        class_mode=\"binary\",\n        target_size=(image_size, image_size),\n        batch_size=batch_size,\n        subset=\"validation\",\n        shuffle=False,\n    )\n\n    test_it = test_datagen.flow_from_directory(\n        TEST_DIR,\n        classes=CLASS_NAMES,\n        class_mode=\"binary\",\n        target_size=(image_size, image_size),\n        batch_size=batch_size,\n        shuffle=False,\n    )\n\n    return train_it, val_it, test_it","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:28:02.661623Z","iopub.execute_input":"2026-10-02T20:28:02.661982Z","iopub.status.idle":"2026-10-02T20:28:02.669708Z","shell.execute_reply.started":"2026-10-02T20:28:02.661958Z","shell.execute_reply":"2026-10-02T20:28:02.668442Z"}},"outputs":[],"execution_count":null},{"id":"3fb334fa","cell_type":"markdown","source":"## 3. Model builder\n\nใช้ `include_top=False` แล้วสร้าง classifier สำหรับ 2 classes  \nขั้นตอนแรก freeze pretrained layers จากนั้นทำ Fine-Tuning โดยเปิดเฉพาะส่วนท้ายของ backbone","metadata":{}},{"id":"b5fe9318","cell_type":"code","source":"def build_transfer_model(model_cls, image_size, preprocess_name):\n    base = model_cls(\n        weights=\"imagenet\",\n        include_top=False,\n        input_shape=(image_size, image_size, 3),\n    )\n\n    for layer in base.layers:\n        layer.trainable = False\n\n    x = GlobalAveragePooling2D()(base.output)\n    x = Dense(256, activation=\"relu\")(x)\n    x = Dropout(0.30)(x)\n    output = Dense(1, activation=\"sigmoid\")(x)\n\n    model = Model(base.input, output, name=f\"{preprocess_name}_transfer\")\n    return model, base\n\n\ndef compile_model(model, learning_rate):\n    model.compile(\n        optimizer=Adam(learning_rate=learning_rate),\n        loss=\"binary_crossentropy\",\n        metrics=[\"accuracy\"],\n    )\n\n\ndef unfreeze_for_finetuning(base_model, last_n_layers):\n    start = max(0, len(base_model.layers) - last_n_layers)\n\n    for i, layer in enumerate(base_model.layers):\n        if i < start:\n            layer.trainable = False\n        else:\n            # BatchNorm ยังคง freeze เพื่อให้ fine-tuning เสถียรขึ้น\n            layer.trainable = not isinstance(layer, tf.keras.layers.BatchNormalization)\n\n    return start","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:28:02.672992Z","iopub.execute_input":"2026-10-02T20:28:02.67339Z","iopub.status.idle":"2026-10-02T20:28:02.690299Z","shell.execute_reply.started":"2026-10-02T20:28:02.673353Z","shell.execute_reply":"2026-10-02T20:28:02.689235Z"}},"outputs":[],"execution_count":null},{"id":"c7675367","cell_type":"markdown","source":"## 4. Training / Fine-Tuning helper","metadata":{}},{"id":"6a2d4c4e","cell_type":"code","source":"def train_and_finetune(\n    model_name,\n    model_cls,\n    image_size,\n    preprocess_fn,\n    batch_size=32,\n    head_epochs=8,\n    fine_tune_epochs=12,\n    fine_tune_layers=20,\n):\n    print(\"=\" * 80)\n    print(model_name)\n\n    train_it, val_it, test_it = make_generators(\n        image_size=image_size,\n        preprocess_fn=preprocess_fn,\n        batch_size=batch_size,\n    )\n\n    model, base = build_transfer_model(\n        model_cls=model_cls,\n        image_size=image_size,\n        preprocess_name=model_name.lower().replace(\" \", \"_\"),\n    )\n\n    # Phase 1: Transfer Learning — pretrained backbone frozen\n    compile_model(model, 1e-4)\n\n    callbacks_head = [\n        EarlyStopping(\n            monitor=\"val_accuracy\",\n            patience=3,\n            mode=\"max\",\n            restore_best_weights=True,\n        ),\n        ReduceLROnPlateau(\n            monitor=\"val_loss\",\n            factor=0.3,\n            patience=2,\n            min_lr=1e-7,\n        ),\n    ]\n\n    history_head = model.fit(\n        train_it,\n        validation_data=val_it,\n        epochs=head_epochs,\n        callbacks=callbacks_head,\n        verbose=1,\n    )\n\n    # Phase 2: Fine-Tuning — open only the last part of the backbone\n    start = unfreeze_for_finetuning(base, fine_tune_layers)\n    print(f\"Fine-tuning from backbone layer index: {start}\")\n\n    compile_model(model, 1e-5)\n\n    callbacks_ft = [\n        EarlyStopping(\n            monitor=\"val_accuracy\",\n            patience=4,\n            mode=\"max\",\n            restore_best_weights=True,\n        ),\n        ReduceLROnPlateau(\n            monitor=\"val_loss\",\n            factor=0.3,\n            patience=2,\n            min_lr=1e-8,\n        ),\n    ]\n\n    history_ft = model.fit(\n        train_it,\n        validation_data=val_it,\n        epochs=fine_tune_epochs,\n        callbacks=callbacks_ft,\n        verbose=1,\n    )\n\n    return model, base, train_it, val_it, test_it, history_head, history_ft","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:28:02.6913Z","iopub.execute_input":"2026-10-02T20:28:02.691607Z","iopub.status.idle":"2026-10-02T20:28:02.706323Z","shell.execute_reply.started":"2026-10-02T20:28:02.691586Z","shell.execute_reply":"2026-10-02T20:28:02.70558Z"}},"outputs":[],"execution_count":null},{"id":"9f88ea9e","cell_type":"markdown","source":"## 5. Classification Report helper","metadata":{}},{"id":"f9e62520","cell_type":"code","source":"def evaluate_on_test(model, test_it, model_name):\n    test_it.reset()\n    probabilities = model.predict(test_it, verbose=1).ravel()\n\n    y_true = test_it.classes.astype(int)\n\n    # threshold จาก validation จะถูกเลือกแยกในแต่ละโมเดลด้วย select_threshold()\n    return probabilities, y_true\n\n\ndef select_threshold(y_true, probabilities):\n    thresholds = np.linspace(0.10, 0.90, 161)\n    best_threshold = 0.50\n    best_acc = -1.0\n\n    for threshold in thresholds:\n        y_pred = (probabilities >= threshold).astype(int)\n        acc = accuracy_score(y_true, y_pred)\n\n        if acc > best_acc:\n            best_acc = acc\n            best_threshold = float(threshold)\n\n    return best_threshold, best_acc\n\n\ndef classification_report_from_test(model, val_it, test_it, model_name):\n    # เลือก threshold จาก validation set เท่านั้น\n    val_it.reset()\n    val_prob = model.predict(val_it, verbose=0).ravel()\n    val_true = val_it.classes.astype(int)\n    threshold, val_acc = select_threshold(val_true, val_prob)\n\n    # ประเมิน test โดยไม่ shuffle\n    test_it.reset()\n    test_prob = model.predict(test_it, verbose=1).ravel()\n    test_true = test_it.classes.astype(int)\n    test_pred = (test_prob >= threshold).astype(int)\n\n    report_text = classification_report(\n        test_true,\n        test_pred,\n        target_names=CLASS_NAMES,\n        digits=4,\n        zero_division=0,\n    )\n    report_dict = classification_report(\n        test_true,\n        test_pred,\n        target_names=CLASS_NAMES,\n        output_dict=True,\n        zero_division=0,\n    )\n\n    test_accuracy = report_dict[\"accuracy\"]\n\n    print(f\"\\n{model_name}\")\n    print(f\"Validation threshold: {threshold:.3f}\")\n    print(f\"Validation accuracy : {val_acc:.4f}\")\n    print(\"\\nClassification Report (TEST SET)\")\n    print(report_text)\n\n    cm = confusion_matrix(test_true, test_pred)\n\n    return {\n        \"model\": model_name,\n        \"threshold\": threshold,\n        \"val_accuracy\": val_acc,\n        \"test_accuracy\": test_accuracy,\n        \"report\": report_dict,\n        \"confusion_matrix\": cm,\n        \"y_true\": test_true,\n        \"y_pred\": test_pred,\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:28:02.707916Z","iopub.execute_input":"2026-10-02T20:28:02.708252Z","iopub.status.idle":"2026-10-02T20:28:02.72321Z","shell.execute_reply.started":"2026-10-02T20:28:02.708222Z","shell.execute_reply":"2026-10-02T20:28:02.722156Z"}},"outputs":[],"execution_count":null},{"id":"aca78461","cell_type":"markdown","source":"## 6. Model 1 — VGG16","metadata":{}},{"id":"485405f1","cell_type":"code","source":"vgg16_model, vgg16_base, vgg16_train_it, vgg16_val_it, vgg16_test_it, vgg16_head_history, vgg16_ft_history = train_and_finetune(\n    model_name=\"VGG16\",\n    model_cls=VGG16,\n    image_size=224,\n    preprocess_fn=vgg16_preprocess,\n    batch_size=32,\n    head_epochs=8,\n    fine_tune_epochs=12,\n    fine_tune_layers=8,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T20:28:02.724405Z","iopub.execute_input":"2026-10-02T20:28:02.72472Z","iopub.status.idle":"2026-10-02T21:35:41.882438Z","shell.execute_reply.started":"2026-10-02T20:28:02.724698Z","shell.execute_reply":"2026-10-02T21:35:41.881264Z"}},"outputs":[],"execution_count":null},{"id":"9389419c","cell_type":"code","source":"vgg16_result = classification_report_from_test(\n    vgg16_model,\n    vgg16_val_it,\n    vgg16_test_it,\n    \"VGG16\",\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T21:35:41.883866Z","iopub.execute_input":"2026-10-02T21:35:41.884239Z","iopub.status.idle":"2026-10-02T21:37:01.083549Z","shell.execute_reply.started":"2026-10-02T21:35:41.884209Z","shell.execute_reply":"2026-10-02T21:37:01.082557Z"}},"outputs":[],"execution_count":null},{"id":"39fb06bd","cell_type":"markdown","source":"## 7. Model 2 — InceptionV3","metadata":{}},{"id":"594d664b","cell_type":"code","source":"inception_model, inception_base, inception_train_it, inception_val_it, inception_test_it, inception_head_history, inception_ft_history = train_and_finetune(\n    model_name=\"InceptionV3\",\n    model_cls=InceptionV3,\n    image_size=299,\n    preprocess_fn=inception_preprocess,\n    batch_size=24,\n    head_epochs=8,\n    fine_tune_epochs=12,\n    fine_tune_layers=30,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T21:37:01.084945Z","iopub.execute_input":"2026-10-02T21:37:01.085306Z","iopub.status.idle":"2026-10-02T22:59:15.853909Z","shell.execute_reply.started":"2026-10-02T21:37:01.085272Z","shell.execute_reply":"2026-10-02T22:59:15.85281Z"}},"outputs":[],"execution_count":null},{"id":"4be13812","cell_type":"code","source":"inception_result = classification_report_from_test(\n    inception_model,\n    inception_val_it,\n    inception_test_it,\n    \"InceptionV3\",\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T22:59:15.855399Z","iopub.execute_input":"2026-10-02T22:59:15.855771Z","iopub.status.idle":"2026-10-02T23:00:51.118948Z","shell.execute_reply.started":"2026-10-02T22:59:15.85572Z","shell.execute_reply":"2026-10-02T23:00:51.117825Z"}},"outputs":[],"execution_count":null},{"id":"c473b42c","cell_type":"markdown","source":"## 8. Model 3 — NASNetLarge","metadata":{}},{"id":"4bf50f83","cell_type":"code","source":"nasnet_model, nasnet_base, nasnet_train_it, nasnet_val_it, nasnet_test_it, nasnet_head_history, nasnet_ft_history = train_and_finetune(\n    model_name=\"NASNetLarge\",\n    model_cls=NASNetLarge,\n    image_size=331,\n    preprocess_fn=nasnet_preprocess,\n    batch_size=8,\n    head_epochs=6,\n    fine_tune_epochs=10,\n    fine_tune_layers=30,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-10-02T23:00:51.120305Z","iopub.execute_input":"2026-10-02T23:00:51.120682Z"}},"outputs":[],"execution_count":null},{"id":"844cf953","cell_type":"code","source":"nasnet_result = classification_report_from_test(\n    nasnet_model,\n    nasnet_val_it,\n    nasnet_test_it,\n    \"NASNetLarge\",\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"b9bb6034","cell_type":"markdown","source":"## 9. เปรียบเทียบ Accuracy จาก Classification Report","metadata":{}},{"id":"b0a9651a","cell_type":"code","source":"results = [vgg16_result, inception_result, nasnet_result]\n\nsummary = pd.DataFrame([\n    {\n        \"Model\": r[\"model\"],\n        \"Validation Accuracy\": r[\"val_accuracy\"],\n        \"Test Accuracy (Classification Report)\": r[\"test_accuracy\"],\n        \"Threshold\": r[\"threshold\"],\n    }\n    for r in results\n])\n\nsummary = summary.sort_values(\"Test Accuracy (Classification Report)\", ascending=False).reset_index(drop=True)\ndisplay(summary)\n\nsummary.to_csv(\"model_classification_report_summary.csv\", index=False)\n\nTARGET_ACCURACY = 0.97\nprint(f\"Minimum required test accuracy: {TARGET_ACCURACY:.2f}\")\n\nfor r in results:\n    status = \"PASS\" if r[\"test_accuracy\"] >= TARGET_ACCURACY else \"BELOW TARGET\"\n    print(f'{r[\"model\"]}: {r[\"test_accuracy\"]:.4f} -> {status}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"d1e7ab24","cell_type":"markdown","source":"## 10. Confusion Matrix","metadata":{}},{"id":"72f95e91","cell_type":"code","source":"fig, axes = plt.subplots(1, 3, figsize=(15, 4))\n\nfor ax, r in zip(axes, results):\n    im = ax.imshow(r[\"confusion_matrix\"])\n    ax.set_title(r[\"model\"])\n    ax.set_xlabel(\"Predicted\")\n    ax.set_ylabel(\"True\")\n    ax.set_xticks([0, 1], CLASS_NAMES)\n    ax.set_yticks([0, 1], CLASS_NAMES)\n\n    for i in range(2):\n        for j in range(2):\n            ax.text(j, i, r[\"confusion_matrix\"][i, j],\n                    ha=\"center\", va=\"center\")\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"0d0125bd","cell_type":"markdown","source":"## หมายเหตุ\n\nAccuracy ที่ใช้ตัดสินเกณฑ์ 0.97 มาจาก `classification_report(..., output_dict=True)[\"accuracy\"]` ของ **test set** โดยตรง\n\nถ้าผลที่รันจริงต่ำกว่า 0.97 จะไม่ถูกสร้างตัวเลขขึ้นมาแทน เพราะต้องใช้ผลการทดสอบจริงของ dataset","metadata":{}}]}