{"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":"9dcd20ab-e108-4d4c-b520-5654ceb659fd","cell_type":"markdown","source":"# Cassava Leaf Disease Classification — Week 6\n","metadata":{}},{"id":"c6a59e35-5570-4cf6-b8c8-2142ef789766","cell_type":"markdown","source":"## Results so far\n\n| Model | Accuracy | Macro F1 |\n|---|---|---|\n| Baseline CNN (Week 3) | 51.46% | 0.2793 |\n| Baseline CNN + Augmentation (Week 4) | 62.40% | 0.2514 |\n| EfficientNetB0 (Week 4) | 66.57% | 0.5347 |\n| EfficientNetB0 + fast pipeline + MixUp + label smoothing (Week 5) | 69.07% | 0.5744 |","metadata":{}},{"id":"42352564-e95c-4066-ba10-a55bfc065469","cell_type":"markdown","source":"## Setup","metadata":{}},{"id":"93598ec8-db42-41e7-9701-e07afd54c4e3","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-09T05:50:08.955671Z","iopub.execute_input":"2026-08-09T05:50:08.956009Z","iopub.status.idle":"2026-08-09T05:50:36.275850Z","shell.execute_reply.started":"2026-08-09T05:50:08.955987Z","shell.execute_reply":"2026-08-09T05:50:36.275065Z"}},"outputs":[],"execution_count":null},{"id":"004dd91a-e5c9-4758-b692-30f719954f71","cell_type":"code","source":"if len(gpus) > 1:\n    strategy = tf.distribute.MirroredStrategy()\nelse:\n    strategy = tf.distribute.get_strategy()\nprint(\"Devices in use:\", strategy.num_replicas_in_sync)\n\ntf.keras.mixed_precision.set_global_policy(\"mixed_float16\")\nprint(tf.keras.mixed_precision.global_policy())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:50:36.277657Z","iopub.execute_input":"2026-08-09T05:50:36.278143Z","iopub.status.idle":"2026-08-09T05:50:36.757391Z","shell.execute_reply.started":"2026-08-09T05:50:36.278117Z","shell.execute_reply":"2026-08-09T05:50:36.756534Z"}},"outputs":[],"execution_count":null},{"id":"474a9af9-8346-4259-8544-662e99ed39e6","cell_type":"markdown","source":"## Load labels, recreate train/val split","metadata":{}},{"id":"a5b09540-3d68-45fc-b59a-5bdab0b77052","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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:50:36.758556Z","iopub.execute_input":"2026-08-09T05:50:36.758898Z","iopub.status.idle":"2026-08-09T05:50:36.880367Z","shell.execute_reply.started":"2026-08-09T05:50:36.758842Z","shell.execute_reply":"2026-08-09T05:50:36.879622Z"}},"outputs":[],"execution_count":null},{"id":"617ef726-8aa9-4331-b680-f2ed7d7d7070","cell_type":"markdown","source":"## Data pipeline","metadata":{}},{"id":"c6f37ed9-3a8c-4e3d-9ec9-2c609b1eeca7","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\n\nprint(\"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)\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, augment_pass=None):\n    do_augment = training if augment_pass is None else augment_pass\n\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 do_augment:\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(mixup, 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\")\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-09T05:50:36.881257Z","iopub.execute_input":"2026-08-09T05:50:36.881449Z","iopub.status.idle":"2026-08-09T05:50:39.466844Z","shell.execute_reply.started":"2026-08-09T05:50:36.881431Z","shell.execute_reply":"2026-08-09T05:50:39.466291Z"}},"outputs":[],"execution_count":null},{"id":"c43e0529-f8f3-4909-8877-ae369d244ba6","cell_type":"markdown","source":"## Focal loss","metadata":{}},{"id":"17bc039f-9960-49c5-a966-65ac07ba744f","cell_type":"code","source":"def categorical_focal_loss(gamma=2.0, alpha=0.25):\n    def loss_fn(y_true, y_pred):\n        y_true = tf.cast(y_true, tf.float32)\n        y_pred = tf.cast(y_pred, tf.float32)\n        y_pred = tf.clip_by_value(y_pred, 1e-7, 1.0 - 1e-7)\n        cross_entropy = -y_true * tf.math.log(y_pred)\n        weight = alpha * tf.pow(1.0 - y_pred, gamma)\n        loss = weight * cross_entropy\n        return tf.reduce_sum(loss, axis=-1)\n    return loss_fn","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:50:39.467946Z","iopub.execute_input":"2026-08-09T05:50:39.468247Z","iopub.status.idle":"2026-08-09T05:50:39.473714Z","shell.execute_reply.started":"2026-08-09T05:50:39.468211Z","shell.execute_reply":"2026-08-09T05:50:39.472833Z"}},"outputs":[],"execution_count":null},{"id":"a0cab0bb-0f15-4336-a7b8-ad587b63f788","cell_type":"markdown","source":"## Model","metadata":{}},{"id":"75500301-9e24-4e2d-a545-94c2d14ce88c","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=categorical_focal_loss(gamma=2.0, alpha=0.25),\n        metrics=[\"accuracy\"]\n    )\n\nmodel.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:50:39.474840Z","iopub.execute_input":"2026-08-09T05:50:39.475270Z","iopub.status.idle":"2026-08-09T05:50:43.043879Z","shell.execute_reply.started":"2026-08-09T05:50:39.475248Z","shell.execute_reply":"2026-08-09T05:50:43.043291Z"}},"outputs":[],"execution_count":null},{"id":"99951f8a-c9dc-455d-8f23-a2880068c033","cell_type":"markdown","source":"## Stage 1: train head, base frozen","metadata":{}},{"id":"36a3cdfa-e2e3-439c-a050-0d2b4f794008","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    callbacks=callbacks_stage1\n)\nprint(f\"Stage 1: {time.time() - start:.1f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:50:43.045807Z","iopub.execute_input":"2026-08-09T05:50:43.046097Z","iopub.status.idle":"2026-08-09T05:57:28.719385Z","shell.execute_reply.started":"2026-08-09T05:50:43.046075Z","shell.execute_reply":"2026-08-09T05:57:28.718707Z"}},"outputs":[],"execution_count":null},{"id":"bd55e901-5bbf-41fa-bfd4-162cd1dae141","cell_type":"markdown","source":"## Stage 2: fine-tune","metadata":{}},{"id":"dac93e05-b5d1-46ee-8b92-7937307eb16a","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=categorical_focal_loss(gamma=2.0, alpha=0.25),\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    callbacks=callbacks_stage2\n)\nprint(f\"Stage 2: {time.time() - start:.1f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T05:57:28.720484Z","iopub.execute_input":"2026-08-09T05:57:28.720972Z","iopub.status.idle":"2026-08-09T06:05:11.780711Z","shell.execute_reply.started":"2026-08-09T05:57:28.720938Z","shell.execute_reply":"2026-08-09T06:05:11.779776Z"}},"outputs":[],"execution_count":null},{"id":"aea6b6dd-88b9-46e8-a0b3-e3d71c62fc71","cell_type":"markdown","source":"## TTA on validation","metadata":{}},{"id":"f5ac332c-6449-4b07-8571-0858d8df43e9","cell_type":"code","source":"N_TTA_ROUNDS = 3\n\ndef tta_predict(df, cache_path, n_rounds=N_TTA_ROUNDS):\n    preds_sum = None\n    for _ in range(n_rounds):\n        ds = build_dataset(df, training=False, cache_path=cache_path, augment_pass=True)\n        preds = model.predict(ds, verbose=0)\n        preds_sum = preds if preds_sum is None else preds_sum + preds\n    return preds_sum / n_rounds\n\nstart = time.time()\nval_preds_proba_tta = tta_predict(val_split, cache_path=\"/kaggle/working/cache/val_cache\")\nval_preds_tta = np.argmax(val_preds_proba_tta, axis=1)\nval_true = val_split[\"label\"].values\nprint(f\"TTA: {time.time() - start:.1f}s\")\n\ntarget_names = [f\"{i}: {label_map[i]}\" for i in range(NUM_CLASSES)]\nprint(classification_report(val_true, val_preds_tta, target_names=target_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T06:05:11.781751Z","iopub.execute_input":"2026-08-09T06:05:11.782293Z","iopub.status.idle":"2026-08-09T06:05:34.925618Z","shell.execute_reply.started":"2026-08-09T06:05:11.782264Z","shell.execute_reply":"2026-08-09T06:05:34.924707Z"}},"outputs":[],"execution_count":null},{"id":"718752f2-a31a-48ec-ab7a-c4da66a6cebc","cell_type":"code","source":"cm = confusion_matrix(val_true, val_preds_tta)\nfig, ax = plt.subplots(figsize=(7, 6))\nsns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Oranges\", xticklabels=range(NUM_CLASSES), yticklabels=range(NUM_CLASSES), ax=ax)\nax.set_xlabel(\"Predicted\")\nax.set_ylabel(\"Actual\")\nax.set_title(\"Confusion Matrix — Week 6\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T06:05:34.926700Z","iopub.execute_input":"2026-08-09T06:05:34.927232Z","iopub.status.idle":"2026-08-09T06:05:35.236770Z","shell.execute_reply.started":"2026-08-09T06:05:34.927204Z","shell.execute_reply":"2026-08-09T06:05:35.235925Z"}},"outputs":[],"execution_count":null},{"id":"28944466-152e-4051-9225-2fd465903540","cell_type":"markdown","source":"## Comparison","metadata":{}},{"id":"abde8d7d-faaf-4c69-96a5-f373eb8c521d","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        \"EfficientNetB0 + focal loss + TTA (Week 6)\"\n    ],\n    \"Accuracy\": [0.5146, 0.6240, 0.6657, 0.6907, accuracy_score(val_true, val_preds_tta)],\n    \"Macro F1\": [0.2793, 0.2514, 0.5347, 0.5744, f1_score(val_true, val_preds_tta, average=\"macro\")]\n})\ncomparison","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T06:05:35.237753Z","iopub.execute_input":"2026-08-09T06:05:35.238114Z","iopub.status.idle":"2026-08-09T06:05:35.265330Z","shell.execute_reply.started":"2026-08-09T06:05:35.238084Z","shell.execute_reply":"2026-08-09T06:05:35.264733Z"}},"outputs":[],"execution_count":null},{"id":"5ca4eecc-177b-4af8-8a9a-243eea018634","cell_type":"code","source":"fig, ax = plt.subplots(figsize=(11, 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-09T06:05:35.266237Z","iopub.execute_input":"2026-08-09T06:05:35.266612Z","iopub.status.idle":"2026-08-09T06:05:35.445816Z","shell.execute_reply.started":"2026-08-09T06:05:35.266589Z","shell.execute_reply":"2026-08-09T06:05:35.445169Z"}},"outputs":[],"execution_count":null},{"id":"93baaa26-6a09-4097-b2a4-db2e0e3b0f92","cell_type":"markdown","source":"## Final submission","metadata":{}},{"id":"835fb23b-1ace-4851-b4cc-35546aa5f469","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\n\ntest_preds_proba_tta = tta_predict(test_df, cache_path=None)\ntest_preds_tta = np.argmax(test_preds_proba_tta, axis=1)\n\nsubmission = pd.DataFrame({\n    \"image_id\": test_df[\"image_id\"],\n    \"label\": test_preds_tta\n})\nsubmission.to_csv(\"submission.csv\", index=False)\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T06:05:35.446713Z","iopub.execute_input":"2026-08-09T06:05:35.447022Z","iopub.status.idle":"2026-08-09T06:05:45.135044Z","shell.execute_reply.started":"2026-08-09T06:05:35.447000Z","shell.execute_reply":"2026-08-09T06:05:45.134353Z"}},"outputs":[],"execution_count":null},{"id":"3878db3e-cd27-41e2-aa57-767cf2a7c42e","cell_type":"markdown","source":"## Cleanup","metadata":{}},{"id":"6128568e-96a8-4ad2-be34-7c2ab9b5a843","cell_type":"code","source":"import shutil\ncache_dir = \"/kaggle/working/cache\"\nif os.path.exists(cache_dir):\n    shutil.rmtree(cache_dir)\n    print(\"Cache removed.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-09T06:05:45.135833Z","iopub.execute_input":"2026-08-09T06:05:45.136195Z","iopub.status.idle":"2026-08-09T06:05:45.613201Z","shell.execute_reply.started":"2026-08-09T06:05:45.136170Z","shell.execute_reply":"2026-08-09T06:05:45.612492Z"}},"outputs":[],"execution_count":null},{"id":"14f240e2-7267-476a-a2eb-58e766b86a52","cell_type":"markdown","source":"## Summary\n\nSwapped class-weighted cross-entropy for focal loss, added TTA at inference. Pipeline unchanged from last week so runtime stayed low.\n","metadata":{}}]}