{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.13"},"papermill":{"default_parameters":{},"duration":890.319222,"end_time":"2026-07-18T08:36:07.290406+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-07-18T08:21:16.971184+00:00","version":"2.7.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"ef4d365a-ad47-462e-966a-cb27ba77b0b3","cell_type":"markdown","source":"# Cassava Leaf Disease Classification — Week 4\n**Week 4 goals (this section):**\n1. Continuing to improving on the baseline.\n2. Adding image augmentation and comparing against the no-augmentation baseline.\n3. Trying transfer learning with a pretrained EfficientNetB0 model.\n4. Comparing all the three models (baseline / augmented / transfer learning) side by side.","metadata":{}},{"id":"88003400-3d41-40a7-8b47-a42b88bba2e3","cell_type":"code","source":"import os\nimport json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\n\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout, BatchNormalization\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nprint(\"TF version:\", tf.__version__)\nprint(\"GPU available:\", tf.config.list_physical_devices('GPU'))\n\nSEED = 42\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nDATA_DIR = \"/kaggle/input/competitions/cassava-leaf-disease-classification\"\nTRAIN_IMG_DIR = os.path.join(DATA_DIR, \"train_images\")\nTEST_IMG_DIR = os.path.join(DATA_DIR, \"test_images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:22.464900Z","iopub.execute_input":"2026-07-25T16:54:22.465136Z","iopub.status.idle":"2026-07-25T16:54:41.028328Z","shell.execute_reply.started":"2026-07-25T16:54:22.465101Z","shell.execute_reply":"2026-07-25T16:54:41.027652Z"}},"outputs":[],"execution_count":null},{"id":"e14b16c6-af34-462f-9bba-f64d8d7aa06f","cell_type":"markdown","source":"## 1. Load labels and metadata","metadata":{}},{"id":"660a54f6-954c-4237-ae3c-6e2b2de25cf6","cell_type":"code","source":"train_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\nprint(train_df.shape)\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:41.029868Z","iopub.execute_input":"2026-07-25T16:54:41.030389Z","iopub.status.idle":"2026-07-25T16:54:41.086653Z","shell.execute_reply.started":"2026-07-25T16:54:41.030364Z","shell.execute_reply":"2026-07-25T16:54:41.085895Z"}},"outputs":[],"execution_count":null},{"id":"3f3116cc-1eb4-40c9-9317-3a5952cae570","cell_type":"code","source":"with open(os.path.join(DATA_DIR, \"label_num_to_disease_map.json\")) as f:\n    label_map = json.load(f)\n\n# JSON keys come in as strings; convert to int for easy mapping\nlabel_map = {int(k): v for k, v in label_map.items()}\nlabel_map","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:41.087478Z","iopub.execute_input":"2026-07-25T16:54:41.087702Z","iopub.status.idle":"2026-07-25T16:54:41.098431Z","shell.execute_reply.started":"2026-07-25T16:54:41.087681Z","shell.execute_reply":"2026-07-25T16:54:41.097761Z"}},"outputs":[],"execution_count":null},{"id":"96a3da61-4db8-42b4-b39c-4f7b99f760f9","cell_type":"markdown","source":"## 2. EDA: Class distribution\n\nThis is the single most important thing to understand before modeling — cassava mosaic disease (label 3) dominates the dataset.","metadata":{}},{"id":"49b64104-7e6a-40ee-83fa-1450f2f54e96","cell_type":"code","source":"train_df[\"label_name\"] = train_df[\"label\"].map(label_map)\n\ncounts = train_df[\"label_name\"].value_counts()\npct = (counts / counts.sum() * 100).round(1)\n\nfig, ax = plt.subplots(figsize=(9, 5))\nsns.barplot(x=counts.values, y=counts.index, ax=ax)\nfor i, (v, p) in enumerate(zip(counts.values, pct.values)):\n    ax.text(v + 50, i, f\"{v} ({p}%)\", va=\"center\")\nax.set_xlabel(\"Number of images\")\nax.set_ylabel(\"\")\nax.set_title(\"Class distribution — Cassava Leaf Disease\")\nplt.tight_layout()\nplt.show()\n\nprint(counts)\nprint(\"\\nNote: a model predicting the majority class for everything would score roughly\",\n      f\"{pct.max()}% accuracy. Track per-class metrics, not just overall accuracy.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:41.099302Z","iopub.execute_input":"2026-07-25T16:54:41.099931Z","iopub.status.idle":"2026-07-25T16:54:41.345974Z","shell.execute_reply.started":"2026-07-25T16:54:41.099908Z","shell.execute_reply":"2026-07-25T16:54:41.344923Z"}},"outputs":[],"execution_count":null},{"id":"f143b406-b462-4766-bf1c-ba093858adc7","cell_type":"markdown","source":"## 3. EDA: Sample images per class","metadata":{}},{"id":"ab6fa866-5a4d-4507-b5d7-4eea03ea3a60","cell_type":"code","source":"fig, axes = plt.subplots(1, 5, figsize=(20, 5))\nfor label_id, ax in zip(sorted(label_map.keys()), axes):\n    sample_id = train_df[train_df[\"label\"] == label_id][\"image_id\"].iloc[0]\n    img = Image.open(os.path.join(TRAIN_IMG_DIR, sample_id))\n    ax.imshow(img)\n    ax.set_title(f\"{label_id}: {label_map[label_id]}\", fontsize=10)\n    ax.axis(\"off\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:41.347063Z","iopub.execute_input":"2026-07-25T16:54:41.347407Z","iopub.status.idle":"2026-07-25T16:54:42.076912Z","shell.execute_reply.started":"2026-07-25T16:54:41.347365Z","shell.execute_reply":"2026-07-25T16:54:42.075979Z"}},"outputs":[],"execution_count":null},{"id":"14e49b18-8eba-479e-ab5e-a89444a0c061","cell_type":"code","source":"# Confirm image resolution is consistent (competition notes 600x800)\nsample_paths = train_df[\"image_id\"].sample(20, random_state=SEED)\nsizes = [Image.open(os.path.join(TRAIN_IMG_DIR, p)).size for p in sample_paths]\nprint(\"Unique sizes found in sample of 20:\", set(sizes))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:42.078362Z","iopub.execute_input":"2026-07-25T16:54:42.079205Z","iopub.status.idle":"2026-07-25T16:54:42.241554Z","shell.execute_reply.started":"2026-07-25T16:54:42.079161Z","shell.execute_reply":"2026-07-25T16:54:42.240680Z"}},"outputs":[],"execution_count":null},{"id":"168470d9-498b-454d-b2e8-f99075ebf9ec","cell_type":"markdown","source":"## 4. Technical challenges identified\n\n- **Class imbalance**: label 3 (Cassava Mosaic Disease) dominates. Overall accuracy alone is misleading; a confusion matrix / per-class report is needed.\n- **Dataset size**: ~21k images at 600x800 is too much to load into memory at once — must stream from disk using a data generator.\n- **Training time**: even scaled-down images will take meaningful time per epoch on Kaggle GPU quota, so early experiments should use small images and few epochs.\n- **Code competition format**: only one public test image is visible; the real test set is hidden and scored on submission, so the notebook must run fully offline at inference time.","metadata":{}},{"id":"b9e549cc-106e-4fca-8301-4d6d29b2dc61","cell_type":"markdown","source":"## 5. Train/validation split (stratified on label)","metadata":{}},{"id":"c9c40d38-ebe8-4fff-9896-c96c095e0319","cell_type":"code","source":"train_df[\"label\"] = train_df[\"label\"].astype(str)  # flow_from_dataframe expects string class labels\n\ntrain_split, val_split = train_test_split(\n    train_df,\n    test_size=0.15,\n    stratify=train_df[\"label\"],\n    random_state=SEED\n)\nprint(\"Train:\", train_split.shape, \" Val:\", val_split.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:42.243785Z","iopub.execute_input":"2026-07-25T16:54:42.244389Z","iopub.status.idle":"2026-07-25T16:54:42.280978Z","shell.execute_reply.started":"2026-07-25T16:54:42.244362Z","shell.execute_reply":"2026-07-25T16:54:42.280321Z"}},"outputs":[],"execution_count":null},{"id":"fc68b49c-e239-4a03-94b0-8a70ddd2b948","cell_type":"markdown","source":"## 6. Data generators (baseline: no augmentation, small images)\n\nStarting at 150x200 as recommended, no augmentation yet — this run is purely to establish a baseline number to beat.","metadata":{}},{"id":"5f7d0933-0697-4c85-a8d6-bd4b574debad","cell_type":"code","source":"IMG_HEIGHT, IMG_WIDTH = 150, 200\nBATCH_SIZE = 32\nNUM_CLASSES = 5\n\ntrain_datagen = ImageDataGenerator(rescale=1./255)\nval_datagen = ImageDataGenerator(rescale=1./255)\n\ntrain_gen = train_datagen.flow_from_dataframe(\n    dataframe=train_split,\n    directory=TRAIN_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=\"label\",\n    target_size=(IMG_HEIGHT, IMG_WIDTH),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=True,\n    seed=SEED\n)\n\nval_gen = val_datagen.flow_from_dataframe(\n    dataframe=val_split,\n    directory=TRAIN_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=\"label\",\n    target_size=(IMG_HEIGHT, IMG_WIDTH),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=False\n)\n\n# class_indices maps string labels -> generator's internal index order; needed later for inference\nclass_indices = train_gen.class_indices\nprint(class_indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:54:42.281714Z","iopub.execute_input":"2026-07-25T16:54:42.281906Z","iopub.status.idle":"2026-07-25T16:55:32.502132Z","shell.execute_reply.started":"2026-07-25T16:54:42.281888Z","shell.execute_reply":"2026-07-25T16:55:32.501492Z"}},"outputs":[],"execution_count":null},{"id":"700993e9-9bf2-4fe6-a4e6-9623f483a12f","cell_type":"markdown","source":"## 7. Baseline CNN\n\nSimple architecture adapted from a CIFAR-10-style CNN, resized for 150x200x3 input and 5 output classes.","metadata":{}},{"id":"0fe59087-9961-4a4b-9637-bb132a6f4260","cell_type":"code","source":"def build_baseline_cnn(input_shape=(IMG_HEIGHT, IMG_WIDTH, 3), num_classes=NUM_CLASSES):\n    model = Sequential([\n        Conv2D(32, (3, 3), activation=\"relu\", padding=\"same\", input_shape=input_shape),\n        BatchNormalization(),\n        MaxPooling2D(2, 2),\n\n        Conv2D(64, (3, 3), activation=\"relu\", padding=\"same\"),\n        BatchNormalization(),\n        MaxPooling2D(2, 2),\n\n        Conv2D(128, (3, 3), activation=\"relu\", padding=\"same\"),\n        BatchNormalization(),\n        MaxPooling2D(2, 2),\n\n        Conv2D(128, (3, 3), activation=\"relu\", padding=\"same\"),\n        BatchNormalization(),\n        MaxPooling2D(2, 2),\n\n        Flatten(),\n        Dense(256, activation=\"relu\"),\n        Dropout(0.5),\n        Dense(num_classes, activation=\"softmax\")\n    ])\n    return model\n\nmodel = build_baseline_cnn()\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:55:32.503163Z","iopub.execute_input":"2026-07-25T16:55:32.503450Z","iopub.status.idle":"2026-07-25T16:55:34.440068Z","shell.execute_reply.started":"2026-07-25T16:55:32.503416Z","shell.execute_reply":"2026-07-25T16:55:34.439436Z"}},"outputs":[],"execution_count":null},{"id":"8780c14e-bf24-4910-a082-3dc1bab78aef","cell_type":"code","source":"# Handle class imbalance with class weights rather than resampling, to keep the baseline simple\nfrom sklearn.utils.class_weight import compute_class_weight\n\nclasses = np.array(sorted(class_indices, key=class_indices.get))\ny_train_int = train_split[\"label\"].values\n\nweights = compute_class_weight(\n    class_weight=\"balanced\",\n    classes=classes,\n    y=y_train_int\n)\nclass_weight_dict = {class_indices[c]: w for c, w in zip(classes, weights)}\nprint(class_weight_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:55:34.441064Z","iopub.execute_input":"2026-07-25T16:55:34.441400Z","iopub.status.idle":"2026-07-25T16:55:34.452655Z","shell.execute_reply.started":"2026-07-25T16:55:34.441364Z","shell.execute_reply":"2026-07-25T16:55:34.451866Z"}},"outputs":[],"execution_count":null},{"id":"ea33ce0d-aa9c-4880-ad26-dc1c681a6524","cell_type":"code","source":"callbacks = [\n    EarlyStopping(monitor=\"val_loss\", patience=3, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2, min_lr=1e-6)\n]\n\nEPOCHS = 10  # keep small for the baseline run; increase once pipeline is confirmed working\n\nhistory = model.fit(\n    train_gen,\n    validation_data=val_gen,\n    epochs=EPOCHS,\n    class_weight=class_weight_dict,\n    callbacks=callbacks\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T16:55:34.453404Z","iopub.execute_input":"2026-07-25T16:55:34.453672Z","iopub.status.idle":"2026-07-25T17:05:40.261437Z","shell.execute_reply.started":"2026-07-25T16:55:34.453630Z","shell.execute_reply":"2026-07-25T17:05:40.260748Z"}},"outputs":[],"execution_count":null},{"id":"7c5202c7-306d-46bc-a114-fe6396e80957","cell_type":"markdown","source":"## 8. Training curves","metadata":{}},{"id":"23ec00e9-3ac8-4bca-a93a-9a70b375ce91","cell_type":"code","source":"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\naxes[0].plot(history.history[\"accuracy\"], label=\"train\")\naxes[0].plot(history.history[\"val_accuracy\"], label=\"val\")\naxes[0].set_title(\"Accuracy\")\naxes[0].set_xlabel(\"Epoch\")\naxes[0].legend()\n\naxes[1].plot(history.history[\"loss\"], label=\"train\")\naxes[1].plot(history.history[\"val_loss\"], label=\"val\")\naxes[1].set_title(\"Loss\")\naxes[1].set_xlabel(\"Epoch\")\naxes[1].legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:05:40.262377Z","iopub.execute_input":"2026-07-25T17:05:40.262713Z","iopub.status.idle":"2026-07-25T17:05:40.825934Z","shell.execute_reply.started":"2026-07-25T17:05:40.262676Z","shell.execute_reply":"2026-07-25T17:05:40.825024Z"}},"outputs":[],"execution_count":null},{"id":"18f3466a-8b25-4f16-b3ed-34d9ccd99cfe","cell_type":"markdown","source":"## 9. Validation evaluation: per-class performance\n\nThis matters more than overall accuracy given the class imbalance.","metadata":{}},{"id":"8c42f1ab-f4d0-4b0e-ba46-0692b65d8479","cell_type":"code","source":"val_gen.reset()\nval_preds_proba = model.predict(val_gen, verbose=1)\nval_preds = np.argmax(val_preds_proba, axis=1)\nval_true = val_gen.classes\n\nidx_to_label = {v: k for k, v in class_indices.items()}\ntarget_names = [f\"{i}: {label_map[int(idx_to_label[i])]}\" for i in range(NUM_CLASSES)]\n\nprint(classification_report(val_true, val_preds, target_names=target_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:05:40.827017Z","iopub.execute_input":"2026-07-25T17:05:40.827555Z","iopub.status.idle":"2026-07-25T17:05:57.145315Z","shell.execute_reply.started":"2026-07-25T17:05:40.827530Z","shell.execute_reply":"2026-07-25T17:05:57.144472Z"}},"outputs":[],"execution_count":null},{"id":"b79cff89-dbc8-42e3-99c5-4fe00e7a4f7e","cell_type":"code","source":"cm = confusion_matrix(val_true, val_preds)\nfig, ax = plt.subplots(figsize=(7, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\", xticklabels=range(NUM_CLASSES), yticklabels=range(NUM_CLASSES), ax=ax)\nax.set_xlabel(\"Predicted\")\nax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix — Baseline CNN\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:05:57.146435Z","iopub.execute_input":"2026-07-25T17:05:57.147111Z","iopub.status.idle":"2026-07-25T17:05:57.332778Z","shell.execute_reply.started":"2026-07-25T17:05:57.147084Z","shell.execute_reply":"2026-07-25T17:05:57.332006Z"}},"outputs":[],"execution_count":null},{"id":"20921106-6618-4083-8554-4d014d502595","cell_type":"markdown","source":"## 10. Inference on the test set and submission file\n\nThis must run fully offline (no internet) since it's a code competition — this baseline uses no external pretrained weights, so it's already safe.","metadata":{}},{"id":"6d2557ea-c61e-4a26-a31f-8c036e1dfd8d","cell_type":"code","source":"test_image_ids = os.listdir(TEST_IMG_DIR)\ntest_df = pd.DataFrame({\"image_id\": test_image_ids})\n\ntest_datagen = ImageDataGenerator(rescale=1./255)\ntest_gen = test_datagen.flow_from_dataframe(\n    dataframe=test_df,\n    directory=TEST_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=None,\n    target_size=(IMG_HEIGHT, IMG_WIDTH),\n    batch_size=BATCH_SIZE,\n    class_mode=None,\n    shuffle=False\n)\n\ntest_preds_proba = model.predict(test_gen, verbose=1)\ntest_preds = np.argmax(test_preds_proba, axis=1)\n\n# Map generator's internal class index back to the original integer label\ntest_df[\"label\"] = [int(idx_to_label[p]) for p in test_preds]\n\nsubmission = test_df[[\"image_id\", \"label\"]]\nsubmission.to_csv(\"submission.csv\", index=False)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:05:57.334287Z","iopub.execute_input":"2026-07-25T17:05:57.334492Z","iopub.status.idle":"2026-07-25T17:05:58.306930Z","shell.execute_reply.started":"2026-07-25T17:05:57.334472Z","shell.execute_reply":"2026-07-25T17:05:58.305932Z"}},"outputs":[],"execution_count":null},{"id":"4b631a47-1b5f-4ea1-b659-e7fda620b9ce","cell_type":"markdown","source":"---\n# Week 4: Improving on the Baseline\n","metadata":{}},{"id":"be0ae898-2c0b-43ff-bce2-720f02709e4f","cell_type":"markdown","source":"## Model 2: Baseline architecture + image augmentation\n\nSame CNN architecture as the Week 3 baseline, same image size, same class weighting — the only change is turning on augmentation in the training generator (rotation, shifts, zoom, flips, brightness). This isolates the effect of augmentation specifically, rather than mixing it with other changes.","metadata":{}},{"id":"90f3aefe-2a8c-415a-a50c-8b4c62bdf37f","cell_type":"code","source":"# Augmented training generator - validation generator stays unaugmented (we never augment val/test data)\ntrain_datagen_aug = ImageDataGenerator(\n    rescale=1./255,\n    rotation_range=30,\n    width_shift_range=0.15,\n    height_shift_range=0.15,\n    shear_range=0.1,\n    zoom_range=0.2,\n    horizontal_flip=True,\n    vertical_flip=True,\n    brightness_range=(0.8, 1.2),\n    fill_mode=\"nearest\"\n)\n\ntrain_gen_aug = train_datagen_aug.flow_from_dataframe(\n    dataframe=train_split,\n    directory=TRAIN_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=\"label\",\n    target_size=(IMG_HEIGHT, IMG_WIDTH),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=True,\n    seed=SEED\n)\n\n# Reuse the same val_gen from Week 3 - validation data should never be augmented","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:05:58.307993Z","iopub.execute_input":"2026-07-25T17:05:58.308427Z","iopub.status.idle":"2026-07-25T17:06:07.275830Z","shell.execute_reply.started":"2026-07-25T17:05:58.308399Z","shell.execute_reply":"2026-07-25T17:06:07.275151Z"}},"outputs":[],"execution_count":null},{"id":"6c682126-2c1b-4e69-8b02-f90ac6d20b2b","cell_type":"code","source":"model_aug = build_baseline_cnn()\nmodel_aug.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\ncallbacks_aug = [\n    EarlyStopping(monitor=\"val_loss\", patience=4, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2, min_lr=1e-6)\n]\n\nEPOCHS_AUG = 15  # augmentation typically needs more epochs to converge than the plain baseline\n\nhistory_aug = model_aug.fit(\n    train_gen_aug,\n    validation_data=val_gen,\n    epochs=EPOCHS_AUG,\n    class_weight=class_weight_dict,\n    callbacks=callbacks_aug\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T17:06:07.276641Z","iopub.execute_input":"2026-07-25T17:06:07.277011Z","execution_failed":"2026-07-25T20:29:43.027Z"}},"outputs":[],"execution_count":null},{"id":"d5c613fc-51ef-478c-b0aa-3aa0f9507922","cell_type":"code","source":"val_gen.reset()\nval_preds_proba_aug = model_aug.predict(val_gen, verbose=1)\nval_preds_aug = np.argmax(val_preds_proba_aug, axis=1)\nval_true_aug = val_gen.classes\n\nprint(\"=== Model 2: Baseline CNN + Augmentation ===\")\nprint(classification_report(val_true_aug, val_preds_aug, target_names=target_names))","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"64885da7-ae7d-4716-9bdc-3354a66f881b","cell_type":"markdown","source":"## 14. Model 3: Transfer learning with EfficientNetB0\n\nThis swaps the from-scratch CNN for a pretrained EfficientNetB0 (trained on ImageNet), with a new classification head on top for our 5 classes. The pretrained convolutional base has already learned general-purpose visual features (edges, textures, shapes) from millions of images, which should give it a much stronger starting point than a small CNN trained from scratch on ~18k images.\n\nTwo-stage training:\n1. Freeze the pretrained base, train only the new head — lets the head adapt quickly without wrecking the pretrained weights.\n2. Unfreeze the top layers of the base and fine-tune at a low learning rate","metadata":{}},{"id":"4ddec65d-507a-468e-973f-84a03430150d","cell_type":"code","source":"from tensorflow.keras.applications import EfficientNetB0\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Input\nfrom tensorflow.keras.models import Model\n\n# EfficientNet expects its own preprocessing (not simple /255 rescaling), and slightly larger images\nIMG_HEIGHT_EFF, IMG_WIDTH_EFF = 224, 224\n\ntrain_datagen_eff = ImageDataGenerator(\n    preprocessing_function=preprocess_input,\n    rotation_range=30,\n    width_shift_range=0.15,\n    height_shift_range=0.15,\n    zoom_range=0.2,\n    horizontal_flip=True,\n    vertical_flip=True,\n    fill_mode=\"nearest\"\n)\nval_datagen_eff = ImageDataGenerator(preprocessing_function=preprocess_input)\n\ntrain_gen_eff = train_datagen_eff.flow_from_dataframe(\n    dataframe=train_split,\n    directory=TRAIN_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=\"label\",\n    target_size=(IMG_HEIGHT_EFF, IMG_WIDTH_EFF),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=True,\n    seed=SEED\n)\n\nval_gen_eff = val_datagen_eff.flow_from_dataframe(\n    dataframe=val_split,\n    directory=TRAIN_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=\"label\",\n    target_size=(IMG_HEIGHT_EFF, IMG_WIDTH_EFF),\n    batch_size=BATCH_SIZE,\n    class_mode=\"categorical\",\n    shuffle=False\n)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"010da009-35f0-4a24-b1a1-15f535a5f537","cell_type":"code","source":"def build_effnet_model(input_shape=(IMG_HEIGHT_EFF, IMG_WIDTH_EFF, 3), num_classes=NUM_CLASSES):\n    base_model = EfficientNetB0(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n    base_model.trainable = False  # freeze for stage 1\n\n    inputs = Input(shape=input_shape)\n    x = base_model(inputs, training=False)\n    x = GlobalAveragePooling2D()(x)\n    x = Dropout(0.3)(x)\n    x = Dense(128, activation=\"relu\")(x)\n    x = Dropout(0.3)(x)\n    outputs = Dense(num_classes, activation=\"softmax\")(x)\n\n    model = Model(inputs, outputs)\n    return model, base_model\n\nmodel_eff, effnet_base = build_effnet_model()\nmodel_eff.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\nmodel_eff.summary()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"c7076442-1da6-43f2-bbe2-75bc64367f4d","cell_type":"markdown","source":"### Stage 1: Train the new head only (base frozen)","metadata":{}},{"id":"5fde0814-04e5-4141-b13b-1ab258779f07","cell_type":"code","source":"callbacks_eff_stage1 = [\n    EarlyStopping(monitor=\"val_loss\", patience=3, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2, min_lr=1e-6)\n]\n\nhistory_eff_stage1 = model_eff.fit(\n    train_gen_eff,\n    validation_data=val_gen_eff,\n    epochs=8,\n    class_weight=class_weight_dict,\n    callbacks=callbacks_eff_stage1\n)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"0e1b09d1-d050-46ff-8793-e1758e549a8d","cell_type":"markdown","source":"### Stage 2: Unfreeze top layers and fine-tune at a low learning rate","metadata":{}},{"id":"66b6d1c7-1712-496c-b549-79eb5d5b32aa","cell_type":"code","source":"effnet_base.trainable = True\n\n# Keep the bottom of the network frozen (general low-level features), only fine-tune the top portion\nFINE_TUNE_AT = len(effnet_base.layers) - 30\nfor layer in effnet_base.layers[:FINE_TUNE_AT]:\n    layer.trainable = False\n\nmodel_eff.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),  # much lower LR for fine-tuning\n    loss=\"categorical_crossentropy\",\n    metrics=[\"accuracy\"]\n)\n\ncallbacks_eff_stage2 = [\n    EarlyStopping(monitor=\"val_loss\", patience=3, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2, min_lr=1e-7)\n]\n\nhistory_eff_stage2 = model_eff.fit(\n    train_gen_eff,\n    validation_data=val_gen_eff,\n    epochs=10,\n    class_weight=class_weight_dict,\n    callbacks=callbacks_eff_stage2\n)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"8c224647-970d-4c5b-aea7-6e3e1fed1928","cell_type":"code","source":"val_gen_eff.reset()\nval_preds_proba_eff = model_eff.predict(val_gen_eff, verbose=1)\nval_preds_eff = np.argmax(val_preds_proba_eff, axis=1)\nval_true_eff = val_gen_eff.classes\n\nprint(\"=== Model 3: EfficientNetB0 Transfer Learning ===\")\nprint(classification_report(val_true_eff, val_preds_eff, target_names=target_names))","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"253bcb89-ee76-463f-83d3-9e8de0eaa881","cell_type":"code","source":"cm_eff = confusion_matrix(val_true_eff, val_preds_eff)\nfig, ax = plt.subplots(figsize=(7, 6))\nsns.heatmap(cm_eff, annot=True, fmt=\"d\", cmap=\"Greens\", xticklabels=range(NUM_CLASSES), yticklabels=range(NUM_CLASSES), ax=ax)\nax.set_xlabel(\"Predicted\")\nax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix - EfficientNetB0 Transfer Learning\")\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.028Z"}},"outputs":[],"execution_count":null},{"id":"397cf778-b6c4-40b3-b4a0-83c9e612ddd5","cell_type":"markdown","source":"## 15. Model comparison\n\nComparing all three approaches on the same validation split (macro-F1 is the more honest metric here given the class imbalance, but both are shown):","metadata":{}},{"id":"de036b0c-9c92-46df-8759-175e98746332","cell_type":"code","source":"from sklearn.metrics import accuracy_score, f1_score\n\ncomparison = pd.DataFrame({\n    \"Model\": [\"Baseline CNN (no aug)\", \"Baseline CNN + Augmentation\", \"EfficientNetB0 (transfer learning)\"],\n    \"Accuracy\": [\n        accuracy_score(val_true, val_preds),\n        accuracy_score(val_true_aug, val_preds_aug),\n        accuracy_score(val_true_eff, val_preds_eff)\n    ],\n    \"Macro F1\": [\n        f1_score(val_true, val_preds, average=\"macro\"),\n        f1_score(val_true_aug, val_preds_aug, average=\"macro\"),\n        f1_score(val_true_eff, val_preds_eff, average=\"macro\")\n    ]\n})\ncomparison","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.029Z"}},"outputs":[],"execution_count":null},{"id":"96c380a0-e598-4811-b2de-9c4dd24853ce","cell_type":"code","source":"fig, ax = plt.subplots(figsize=(9, 5))\nx = np.arange(len(comparison))\nwidth = 0.35\nax.bar(x - width/2, comparison[\"Accuracy\"], width, label=\"Accuracy\")\nax.bar(x + width/2, comparison[\"Macro F1\"], width, label=\"Macro F1\")\nax.set_xticks(x)\nax.set_xticklabels(comparison[\"Model\"], rotation=15, ha=\"right\")\nax.set_ylim(0, 1)\nax.set_title(\"Model comparison - Week 3 baseline vs Week 4 improvements\")\nax.legend()\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-07-25T20:29:43.029Z"}},"outputs":[],"execution_count":null},{"id":"64b5275f-7f79-4014-96bf-957f0e6d90ef","cell_type":"markdown","source":"## 16. Final submission (best-performing model)","metadata":{}},{"id":"1c867d96-6d34-468a-a0a0-615a8745a777","cell_type":"code","source":"test_image_ids = os.listdir(TEST_IMG_DIR)\ntest_df_final = pd.DataFrame({\"image_id\": test_image_ids})\n\ntest_datagen_eff = ImageDataGenerator(preprocessing_function=preprocess_input)\ntest_gen_eff = test_datagen_eff.flow_from_dataframe(\n    dataframe=test_df_final,\n    directory=TEST_IMG_DIR,\n    x_col=\"image_id\",\n    y_col=None,\n    target_size=(IMG_HEIGHT_EFF, IMG_WIDTH_EFF),\n    batch_size=BATCH_SIZE,\n    class_mode=None,\n    shuffle=False\n)\n\ntest_preds_proba_final = model_eff.predict(test_gen_eff, verbose=1)\ntest_preds_final = np.argmax(test_preds_proba_final, axis=1)\n\ntest_df_final[\"label\"] = [int(idx_to_label[p]) for p in test_preds_final]\n\nsubmission_final = test_df_final[[\"image_id\", \"label\"]]\nsubmission_final.to_csv(\"submission.csv\", index=False)\nsubmission_final.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-07-25T20:30:12.093969Z","iopub.status.idle":"2026-07-25T20:30:12.094380Z","shell.execute_reply.started":"2026-07-25T20:30:12.094158Z","shell.execute_reply":"2026-07-25T20:30:12.094180Z"}},"outputs":[],"execution_count":null}]}