{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"3a8b676b-ac8a-4676-875c-65a726aaebf0","cell_type":"markdown","source":"# Cassava Leaf Disease Classification — Week 5\n","metadata":{}},{"id":"eb6c3179-9e30-438b-8adc-ccce8ba36785","cell_type":"markdown","source":"## 1. Setup","metadata":{}},{"id":"1ad621cd-a0ac-44c4-947c-dc2c00d9604c","cell_type":"code","source":"import os\nimport time\nimport json\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport tensorflow as tf\nfrom tensorflow.keras.applications import EfficientNetB0\nfrom tensorflow.keras.applications.efficientnet import preprocess_input\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Input, Dense, Dropout\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score\n\nprint(\"TF version:\", tf.__version__)\ngpus = tf.config.list_physical_devices('GPU')\nprint(\"GPUs available:\", gpus)\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-08-01T07:01:23.762175Z","iopub.execute_input":"2026-08-01T07:01:23.762480Z","iopub.status.idle":"2026-08-01T07:01:42.864393Z","shell.execute_reply.started":"2026-08-01T07:01:23.762455Z","shell.execute_reply":"2026-08-01T07:01:42.863503Z"}},"outputs":[],"execution_count":null},{"id":"5650885b-9047-4763-b46f-e878a4ad49f0","cell_type":"code","source":"# Use both GPUs if available, mixed precision for speed on T4 tensor cores\nif len(gpus) > 1:\n    strategy = tf.distribute.MirroredStrategy()\nelse:\n    strategy = tf.distribute.get_strategy()\nprint(\"Number of devices in use:\", strategy.num_replicas_in_sync)\n\ntf.keras.mixed_precision.set_global_policy(\"mixed_float16\")\nprint(\"Mixed precision policy:\", tf.keras.mixed_precision.global_policy())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:01:42.866075Z","iopub.execute_input":"2026-08-01T07:01:42.866540Z","iopub.status.idle":"2026-08-01T07:01:43.348905Z","shell.execute_reply.started":"2026-08-01T07:01:42.866514Z","shell.execute_reply":"2026-08-01T07:01:43.348000Z"}},"outputs":[],"execution_count":null},{"id":"9b83f1b1-6517-4db9-a8fd-7ba8e39ea42f","cell_type":"markdown","source":"## 2. Load labels and recreate the same train/val split\n\nSame seed and split ratio as Week 3/4, so validation results stay comparable to the numbers in the recap table above.","metadata":{}},{"id":"3dfec93e-7b65-426e-89f4-420863ea4095","cell_type":"code","source":"train_df = pd.read_csv(os.path.join(DATA_DIR, \"train.csv\"))\n\nwith open(os.path.join(DATA_DIR, \"label_num_to_disease_map.json\")) as f:\n    label_map = json.load(f)\nlabel_map = {int(k): v for k, v in label_map.items()}\n\ntrain_df[\"filepath\"] = train_df[\"image_id\"].apply(lambda x: os.path.join(TRAIN_IMG_DIR, x))\n\nNUM_CLASSES = 5\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)\n\nfrom sklearn.utils.class_weight import compute_class_weight\nclasses = np.arange(NUM_CLASSES)\nweights = compute_class_weight(class_weight=\"balanced\", classes=classes, y=train_split[\"label\"].values)\nclass_weight_dict = {i: w for i, w in zip(classes, weights)}\nprint(class_weight_dict)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:01:43.350171Z","iopub.execute_input":"2026-08-01T07:01:43.350526Z","iopub.status.idle":"2026-08-01T07:01:43.435321Z","shell.execute_reply.started":"2026-08-01T07:01:43.350500Z","shell.execute_reply":"2026-08-01T07:01:43.434553Z"}},"outputs":[],"execution_count":null},{"id":"93bb555f-cacd-4b57-95fd-68c286777e70","cell_type":"markdown","source":"## 3. Fast tf.data pipeline (with caching, augmentation, MixUp)\n","metadata":{}},{"id":"1a04c59e-87f0-4e00-aa39-c3397fa6c0d2","cell_type":"code","source":"IMG_SIZE = 224\nPER_REPLICA_BATCH = 32\nBATCH_SIZE = PER_REPLICA_BATCH * max(strategy.num_replicas_in_sync, 1)\nAUTOTUNE = tf.data.AUTOTUNE\nUSE_MIXUP = True  # set to False for a quick fallback if MixUp seems to hurt validation performance\n\nprint(\"Global batch size:\", BATCH_SIZE)\n\ndef decode_img(filepath, label):\n    img = tf.io.read_file(filepath)\n    img = tf.io.decode_jpeg(img, channels=3)\n    img = tf.image.resize(img, [IMG_SIZE, IMG_SIZE], method=\"bilinear\")\n    img = tf.cast(img, tf.uint8)  # store cache as uint8 to keep disk/memory usage small\n    return img, label\n\ndef augment(img):\n    img = tf.image.random_flip_left_right(img)\n    img = tf.image.random_flip_up_down(img)\n    img = tf.image.rot90(img, k=tf.random.uniform([], 0, 4, dtype=tf.int32))\n    img = tf.image.random_brightness(img, 0.15)\n    img = tf.image.random_contrast(img, 0.85, 1.15)\n    img = tf.image.random_saturation(img, 0.85, 1.15)\n    img = tf.clip_by_value(img, 0.0, 255.0)\n    return img\n\ndef sample_beta(alpha, batch_size):\n    g1 = tf.random.gamma([batch_size], alpha)\n    g2 = tf.random.gamma([batch_size], alpha)\n    return g1 / (g1 + g2)\n\ndef mixup(images, labels, alpha=0.2):\n    images = tf.cast(images, tf.float32)\n    labels = tf.cast(labels, tf.float32)\n    batch_size = tf.shape(images)[0]\n    lam = tf.cast(sample_beta(alpha, batch_size), tf.float32)\n    idx = tf.random.shuffle(tf.range(batch_size))\n    images2 = tf.gather(images, idx)\n    labels2 = tf.gather(labels, idx)\n    lam_x = lam[:, tf.newaxis, tf.newaxis, tf.newaxis]\n    lam_y = lam[:, tf.newaxis]\n    images = lam_x * images + (1.0 - lam_x) * images2\n    labels = lam_y * labels + (1.0 - lam_y) * labels2\n    return images, labels  \n\ndef build_dataset(df, training, cache_path=None):\n    filepaths = df[\"filepath\"].values\n    labels = tf.keras.utils.to_categorical(df[\"label\"].values, num_classes=NUM_CLASSES)\n\n    ds = tf.data.Dataset.from_tensor_slices((filepaths, labels))\n    if training:\n        ds = ds.shuffle(buffer_size=len(df), seed=SEED, reshuffle_each_iteration=True)\n\n    ds = ds.map(decode_img, num_parallel_calls=AUTOTUNE)\n\n    if cache_path is not None:\n        ds = ds.cache(cache_path)\n\n    ds = ds.map(lambda img, lb: (tf.cast(img, tf.float32), lb), num_parallel_calls=AUTOTUNE)\n\n    if training:\n        ds = ds.map(lambda img, lb: (augment(img), lb), num_parallel_calls=AUTOTUNE)\n\n    ds = ds.map(lambda img, lb: (preprocess_input(img), lb), num_parallel_calls=AUTOTUNE)\n    ds = ds.batch(BATCH_SIZE)\n\n    if training and USE_MIXUP:\n        ds = ds.map(lambda img, lb: mixup(img, lb), num_parallel_calls=AUTOTUNE)\n\n    ds = ds.prefetch(AUTOTUNE)\n    return ds\n\nos.makedirs(\"/kaggle/working/cache\", exist_ok=True)\ntrain_ds = build_dataset(train_split, training=True, cache_path=\"/kaggle/working/cache/train_cache\")\nval_ds = build_dataset(val_split, training=False, cache_path=\"/kaggle/working/cache/val_cache\")\n\n# A separate, non-shuffled, non-augmented version of the val set for generating predictions later\nval_ds_eval = build_dataset(val_split, training=False, cache_path=\"/kaggle/working/cache/val_cache\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:01:43.436482Z","iopub.execute_input":"2026-08-01T07:01:43.436858Z","iopub.status.idle":"2026-08-01T07:01:45.688942Z","shell.execute_reply.started":"2026-08-01T07:01:43.436831Z","shell.execute_reply":"2026-08-01T07:01:45.688188Z"}},"outputs":[],"execution_count":null},{"id":"d1f490d8-733f-4cfa-83f3-ab4670b1a538","cell_type":"markdown","source":"## 4. Build EfficientNetB0 (transfer learning), mixed-precision safe\n\nSame base architecture idea as Week 4, built inside the multi-GPU strategy scope this time. The final Dense layer is explicitly cast back to float32 — a standard requirement when using mixed precision, since softmax probabilities need full precision for stable loss calculation.","metadata":{}},{"id":"37d87c49-feec-4582-bcd7-4a15dd653adf","cell_type":"code","source":"def build_effnet_model(input_shape=(IMG_SIZE, IMG_SIZE, 3), num_classes=NUM_CLASSES):\n    base_model = EfficientNetB0(include_top=False, weights=\"imagenet\", input_shape=input_shape)\n    base_model.trainable = False\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\", dtype=\"float32\")(x)\n\n    model = Model(inputs, outputs)\n    return model, base_model\n\nwith strategy.scope():\n    model, effnet_base = build_effnet_model()\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),\n        loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n        metrics=[\"accuracy\"]\n    )\n\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:01:45.690831Z","iopub.execute_input":"2026-08-01T07:01:45.691070Z","iopub.status.idle":"2026-08-01T07:01:49.645942Z","shell.execute_reply.started":"2026-08-01T07:01:45.691047Z","shell.execute_reply":"2026-08-01T07:01:49.645237Z"}},"outputs":[],"execution_count":null},{"id":"776a6e49-030a-42bb-b7d1-9af141b8c3d7","cell_type":"markdown","source":"## 5. Stage 1: train the new head (base frozen)","metadata":{}},{"id":"e6b4df2f-bbde-4e27-a3c3-6fd8d09c0fd7","cell_type":"code","source":"callbacks_stage1 = [\n    EarlyStopping(monitor=\"val_loss\", patience=2, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=1, min_lr=1e-6)\n]\n\nstart = time.time()\nhistory_stage1 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=6,\n    class_weight=class_weight_dict,\n    callbacks=callbacks_stage1\n)\nprint(f\"Stage 1 took {time.time() - start:.1f} seconds\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:01:49.646901Z","iopub.execute_input":"2026-08-01T07:01:49.647374Z","iopub.status.idle":"2026-08-01T07:07:26.929675Z","shell.execute_reply.started":"2026-08-01T07:01:49.647346Z","shell.execute_reply":"2026-08-01T07:07:26.928677Z"}},"outputs":[],"execution_count":null},{"id":"a3e27ac0-f0eb-4f10-b401-596cbff22fab","cell_type":"markdown","source":"## 6. Stage 2: unfreeze and fine-tune at a low learning rate\n\nUnfreezing more of the base than last week (last 60 layers instead of 30) since the faster pipeline gives some extra time budget for it.","metadata":{}},{"id":"1fe1179d-5ff5-424c-a42a-00aab665b51c","cell_type":"code","source":"effnet_base.trainable = True\nFINE_TUNE_AT = len(effnet_base.layers) - 60\nfor layer in effnet_base.layers[:FINE_TUNE_AT]:\n    layer.trainable = False\n\nwith strategy.scope():\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),\n        loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n        metrics=[\"accuracy\"]\n    )\n\ncallbacks_stage2 = [\n    EarlyStopping(monitor=\"val_loss\", patience=2, restore_best_weights=True),\n    ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=1, min_lr=1e-7)\n]\n\nstart = time.time()\nhistory_stage2 = model.fit(\n    train_ds,\n    validation_data=val_ds,\n    epochs=10,\n    class_weight=class_weight_dict,\n    callbacks=callbacks_stage2\n)\nprint(f\"Stage 2 took {time.time() - start:.1f} seconds\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:07:26.930951Z","iopub.execute_input":"2026-08-01T07:07:26.931317Z","iopub.status.idle":"2026-08-01T07:15:44.860497Z","shell.execute_reply.started":"2026-08-01T07:07:26.931290Z","shell.execute_reply":"2026-08-01T07:15:44.859664Z"}},"outputs":[],"execution_count":null},{"id":"e3148feb-345d-4548-8197-8decb47e61d0","cell_type":"markdown","source":"## 7. Training curves","metadata":{}},{"id":"ea32e022-0150-47f7-82c3-4e2439f48d01","cell_type":"code","source":"acc = history_stage1.history[\"accuracy\"] + history_stage2.history[\"accuracy\"]\nval_acc = history_stage1.history[\"val_accuracy\"] + history_stage2.history[\"val_accuracy\"]\nloss = history_stage1.history[\"loss\"] + history_stage2.history[\"loss\"]\nval_loss = history_stage1.history[\"val_loss\"] + history_stage2.history[\"val_loss\"]\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\naxes[0].plot(acc, label=\"train\")\naxes[0].plot(val_acc, label=\"val\")\naxes[0].axvline(len(history_stage1.history[\"accuracy\"]) - 0.5, color=\"gray\", linestyle=\"--\", label=\"fine-tune starts\")\naxes[0].set_title(\"Accuracy\")\naxes[0].legend()\n\naxes[1].plot(loss, label=\"train\")\naxes[1].plot(val_loss, label=\"val\")\naxes[1].axvline(len(history_stage1.history[\"loss\"]) - 0.5, color=\"gray\", linestyle=\"--\", label=\"fine-tune starts\")\naxes[1].set_title(\"Loss\")\naxes[1].legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:44.861830Z","iopub.execute_input":"2026-08-01T07:15:44.862229Z","iopub.status.idle":"2026-08-01T07:15:45.338148Z","shell.execute_reply.started":"2026-08-01T07:15:44.862201Z","shell.execute_reply":"2026-08-01T07:15:45.337384Z"}},"outputs":[],"execution_count":null},{"id":"189d7b4f-55e4-4dcd-b08c-90ced7971ee4","cell_type":"markdown","source":"## 8. Validation evaluation: per-class performance","metadata":{}},{"id":"6dc9aa3c-ba77-40b1-8523-970ed604f1e0","cell_type":"code","source":"val_preds_proba = model.predict(val_ds_eval, verbose=1)\nval_preds = np.argmax(val_preds_proba, axis=1)\nval_true = val_split[\"label\"].values\n\ntarget_names = [f\"{i}: {label_map[i]}\" for i in range(NUM_CLASSES)]\nprint(classification_report(val_true, val_preds, target_names=target_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:45.339116Z","iopub.execute_input":"2026-08-01T07:15:45.339475Z","iopub.status.idle":"2026-08-01T07:15:59.158015Z","shell.execute_reply.started":"2026-08-01T07:15:45.339446Z","shell.execute_reply":"2026-08-01T07:15:59.157054Z"}},"outputs":[],"execution_count":null},{"id":"d6d97a23-2a02-43b3-95d2-1f4f1c7527b9","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=\"Purples\", xticklabels=range(NUM_CLASSES), yticklabels=range(NUM_CLASSES), ax=ax)\nax.set_xlabel(\"Predicted\")\nax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix — Week 5 EfficientNetB0 (fast pipeline + MixUp + label smoothing)\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:59.159049Z","iopub.execute_input":"2026-08-01T07:15:59.159319Z","iopub.status.idle":"2026-08-01T07:15:59.404248Z","shell.execute_reply.started":"2026-08-01T07:15:59.159294Z","shell.execute_reply":"2026-08-01T07:15:59.403367Z"}},"outputs":[],"execution_count":null},{"id":"ef8fbbd4-d5af-4530-991c-0fb176dda596","cell_type":"markdown","source":"## 9. Comparison against previous weeks","metadata":{}},{"id":"5bece98d-05bf-49fe-8c9b-9eb5c216f79c","cell_type":"code","source":"comparison = pd.DataFrame({\n    \"Model\": [\n        \"Baseline CNN (Week 3)\",\n        \"Baseline CNN + Augmentation (Week 4)\",\n        \"EfficientNetB0 (Week 4)\",\n        \"EfficientNetB0 + fast pipeline + MixUp + label smoothing (Week 5)\"\n    ],\n    \"Accuracy\": [0.5146, 0.6240, 0.6657, accuracy_score(val_true, val_preds)],\n    \"Macro F1\": [0.2793, 0.2514, 0.5347, f1_score(val_true, val_preds, average=\"macro\")]\n})\ncomparison","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:59.405392Z","iopub.execute_input":"2026-08-01T07:15:59.405849Z","iopub.status.idle":"2026-08-01T07:15:59.432116Z","shell.execute_reply.started":"2026-08-01T07:15:59.405821Z","shell.execute_reply":"2026-08-01T07:15:59.431399Z"}},"outputs":[],"execution_count":null},{"id":"9a8a3449-f954-4adf-9f62-85808b3fe062","cell_type":"code","source":"fig, ax = plt.subplots(figsize=(10, 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=20, ha=\"right\")\nax.set_ylim(0, 1)\nax.set_title(\"Model comparison across all weeks\")\nax.legend()\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:59.433227Z","iopub.execute_input":"2026-08-01T07:15:59.434044Z","iopub.status.idle":"2026-08-01T07:15:59.617144Z","shell.execute_reply.started":"2026-08-01T07:15:59.434014Z","shell.execute_reply":"2026-08-01T07:15:59.616294Z"}},"outputs":[],"execution_count":null},{"id":"19c24530-5fee-4c06-8964-83ac440be4e4","cell_type":"markdown","source":"## 10. Final submission","metadata":{}},{"id":"ab5d6af2-5539-4127-9452-8c4212e86a41","cell_type":"code","source":"test_image_ids = os.listdir(TEST_IMG_DIR)\ntest_df = pd.DataFrame({\"image_id\": test_image_ids})\ntest_df[\"filepath\"] = test_df[\"image_id\"].apply(lambda x: os.path.join(TEST_IMG_DIR, x))\ntest_df[\"label\"] = 0  # placeholder, unused, just needed so build_dataset's labels arg has something to slice\n\ntest_ds = build_dataset(test_df, training=False, cache_path=None)\n\ntest_preds_proba = model.predict(test_ds, verbose=1)\ntest_preds = np.argmax(test_preds_proba, axis=1)\n\nsubmission = pd.DataFrame({\n    \"image_id\": test_df[\"image_id\"],\n    \"label\": test_preds\n})\nsubmission.to_csv(\"submission.csv\", index=False)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-01T07:15:59.618179Z","iopub.execute_input":"2026-08-01T07:15:59.618648Z","iopub.status.idle":"2026-08-01T07:16:09.006517Z","shell.execute_reply.started":"2026-08-01T07:15:59.618607Z","shell.execute_reply":"2026-08-01T07:16:09.005784Z"}},"outputs":[],"execution_count":null}]}