{"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":"1f0acfae-49b7-4b24-8800-ac20309f2d0b","cell_type":"markdown","source":"# Cassava Leaf Disease Classification — Week 8\nGoal: beat Week 7 (77.85% accuracy, 0.6194 macro F1).","metadata":{}},{"id":"5e947d8b-07c4-4292-910d-6494c171a24f","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 |\n| EfficientNetB0 + focal loss + TTA (Week 6) | 76.23% | 0.5856 |\n| EfficientNetB0 + progressive resizing + cutout + focal loss + TTA (Week 7) | 77.85% | 0.6194 |","metadata":{}},{"id":"8785448c-21b0-489a-b0c1-93e4177dd226","cell_type":"markdown","source":"## Setup","metadata":{}},{"id":"4f98257d-a6c2-479f-bae1-3fedf5d1f5f6","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)\nassert len(gpus) > 0, \"No GPU detected. Stop here and fix Settings > Accelerator before running the rest of the notebook.\"\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-24T04:07:08.807916Z","iopub.execute_input":"2026-08-24T04:07:08.808090Z","iopub.status.idle":"2026-08-24T04:07:29.871687Z","shell.execute_reply.started":"2026-08-24T04:07:08.808070Z","shell.execute_reply":"2026-08-24T04:07:29.870754Z"}},"outputs":[],"execution_count":null},{"id":"180f59a2-9380-45cf-98c2-df242010e88c","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-24T04:07:29.873211Z","iopub.execute_input":"2026-08-24T04:07:29.873777Z","iopub.status.idle":"2026-08-24T04:07:30.346453Z","shell.execute_reply.started":"2026-08-24T04:07:29.873752Z","shell.execute_reply":"2026-08-24T04:07:30.345717Z"}},"outputs":[],"execution_count":null},{"id":"f91fd678-6a19-4194-bd3c-6d05d192bc60","cell_type":"markdown","source":"## Load labels, recreate train/val split","metadata":{}},{"id":"49168ebd-c48d-40ef-839d-92fe0ec9e513","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-24T04:07:30.347404Z","iopub.execute_input":"2026-08-24T04:07:30.347857Z","iopub.status.idle":"2026-08-24T04:07:30.431912Z","shell.execute_reply.started":"2026-08-24T04:07:30.347822Z","shell.execute_reply":"2026-08-24T04:07:30.431016Z"}},"outputs":[],"execution_count":null},{"id":"6ac958b0-df79-468b-88e7-1b70f906db3f","cell_type":"markdown","source":"## Data pipeline","metadata":{}},{"id":"f7bd83f2-032a-485c-a22e-3a69f30339c1","cell_type":"code","source":"PER_REPLICA_BATCH = 32\nAUTOTUNE = tf.data.AUTOTUNE\nUSE_MIXUP = False\nUSE_CUTOUT = True\n\nBATCH_SIZE = PER_REPLICA_BATCH * max(strategy.num_replicas_in_sync, 1)\nprint(\"Batch size:\", BATCH_SIZE)\n\ndef decode_img(filepath, label, img_size):\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 cutout(img, pct=0.2):\n    shape = tf.shape(img)\n    h, w = shape[0], shape[1]\n    cut_h = tf.cast(tf.cast(h, tf.float32) * pct, tf.int32)\n    cut_w = tf.cast(tf.cast(w, tf.float32) * pct, tf.int32)\n    cy = tf.random.uniform([], 0, h, dtype=tf.int32)\n    cx = tf.random.uniform([], 0, w, dtype=tf.int32)\n    y1 = tf.clip_by_value(cy - cut_h // 2, 0, h)\n    y2 = tf.clip_by_value(cy + cut_h // 2, 0, h)\n    x1 = tf.clip_by_value(cx - cut_w // 2, 0, w)\n    x2 = tf.clip_by_value(cx + cut_w // 2, 0, w)\n    mask = tf.ones((y2 - y1, x2 - x1, 3), dtype=img.dtype)\n    mask = tf.pad(mask, [[y1, h - y2], [x1, w - x2], [0, 0]])\n    return img * (1.0 - mask)\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, img_size, 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(lambda fp, lb: decode_img(fp, lb, img_size), 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        if USE_CUTOUT:\n            ds = ds.map(lambda img, lb: (cutout(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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:07:30.433000Z","iopub.execute_input":"2026-08-24T04:07:30.433436Z","iopub.status.idle":"2026-08-24T04:07:30.449491Z","shell.execute_reply.started":"2026-08-24T04:07:30.433397Z","shell.execute_reply":"2026-08-24T04:07:30.448791Z"}},"outputs":[],"execution_count":null},{"id":"0d224823-10b5-44b6-baf8-27e96cf9316c","cell_type":"markdown","source":"## Focal loss","metadata":{}},{"id":"24181d8e-67fe-462b-a253-042aad466d6c","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-24T04:07:30.451803Z","iopub.execute_input":"2026-08-24T04:07:30.452027Z","iopub.status.idle":"2026-08-24T04:07:30.468842Z","shell.execute_reply.started":"2026-08-24T04:07:30.452005Z","shell.execute_reply":"2026-08-24T04:07:30.468184Z"}},"outputs":[],"execution_count":null},{"id":"e6957403-b06d-409a-8366-b4ac8fdeadf6","cell_type":"markdown","source":"## Model builder","metadata":{}},{"id":"c0a6abe3-2e36-467a-8c83-af2b3f1aaf07","cell_type":"code","source":"def build_effnet_model(input_shape, 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:07:30.469917Z","iopub.execute_input":"2026-08-24T04:07:30.470283Z","iopub.status.idle":"2026-08-24T04:07:30.482552Z","shell.execute_reply.started":"2026-08-24T04:07:30.470252Z","shell.execute_reply":"2026-08-24T04:07:30.481772Z"}},"outputs":[],"execution_count":null},{"id":"3a6ac351-884b-48df-a950-f95e4145f7d5","cell_type":"markdown","source":"## Stage 1: train head at 160x160\n\nEpoch counts trimmed from last week (6 -> 4) to keep total runtime down. This model is kept for the ensemble at the end rather than discarded.","metadata":{}},{"id":"d44b16ee-ccba-4a71-90b5-05635fd2da92","cell_type":"code","source":"STAGE1_SIZE = 160\n\ntrain_ds_s1 = build_dataset(train_split, training=True, img_size=STAGE1_SIZE, cache_path=\"/kaggle/working/cache/train_160\")\nval_ds_s1 = build_dataset(val_split, training=False, img_size=STAGE1_SIZE, cache_path=\"/kaggle/working/cache/val_160\")\n\nwith strategy.scope():\n    model_s1, effnet_base_s1 = build_effnet_model(input_shape=(STAGE1_SIZE, STAGE1_SIZE, 3))\n    model_s1.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\ncallbacks_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_s1.fit(\n    train_ds_s1,\n    validation_data=val_ds_s1,\n    epochs=4,\n    callbacks=callbacks_stage1\n)\nprint(f\"Stage 1 ({STAGE1_SIZE}x{STAGE1_SIZE}): {time.time() - start:.1f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:07:30.483419Z","iopub.execute_input":"2026-08-24T04:07:30.483793Z","iopub.status.idle":"2026-08-24T04:10:45.131389Z","shell.execute_reply.started":"2026-08-24T04:07:30.483769Z","shell.execute_reply":"2026-08-24T04:10:45.130543Z"}},"outputs":[],"execution_count":null},{"id":"8ad0dc39-1c7a-4357-b327-00ce31133412","cell_type":"markdown","source":"## Stage 2: fine-tune at 224x224\n\nEpoch counts trimmed from last week (10 -> 6).","metadata":{}},{"id":"0e088935-7153-4574-a2f5-97261561c961","cell_type":"code","source":"STAGE2_SIZE = 224\n\ntrain_ds_s2 = build_dataset(train_split, training=True, img_size=STAGE2_SIZE, cache_path=\"/kaggle/working/cache/train_224\")\nval_ds_s2 = build_dataset(val_split, training=False, img_size=STAGE2_SIZE, cache_path=\"/kaggle/working/cache/val_224\")\n\nwith strategy.scope():\n    model_s2, effnet_base_s2 = build_effnet_model(input_shape=(STAGE2_SIZE, STAGE2_SIZE, 3))\n    model_s2.set_weights(model_s1.get_weights())\n\n    effnet_base_s2.trainable = True\n    FINE_TUNE_AT = len(effnet_base_s2.layers) - 60\n    for layer in effnet_base_s2.layers[:FINE_TUNE_AT]:\n        layer.trainable = False\n\n    model_s2.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_s2.fit(\n    train_ds_s2,\n    validation_data=val_ds_s2,\n    epochs=6,\n    callbacks=callbacks_stage2\n)\nprint(f\"Stage 2 ({STAGE2_SIZE}x{STAGE2_SIZE}): {time.time() - start:.1f}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:10:45.132590Z","iopub.execute_input":"2026-08-24T04:10:45.132926Z","iopub.status.idle":"2026-08-24T04:16:13.524941Z","shell.execute_reply.started":"2026-08-24T04:10:45.132890Z","shell.execute_reply":"2026-08-24T04:16:13.524280Z"}},"outputs":[],"execution_count":null},{"id":"9373eeab-4c40-45e5-bdc1-f9df4693e085","cell_type":"markdown","source":"## Ensemble: average Stage 1 (160) and Stage 2 (224) predictions\n\nBoth models get TTA individually, then their averaged probabilities are compared against Stage 2 alone. Stage 2 is expected to be the stronger model on its own (it's fine-tuned and at full resolution), so it's weighted more heavily in the average.","metadata":{}},{"id":"8d4874c8-a41a-4ac2-97cb-cde770ed8121","cell_type":"code","source":"N_TTA_ROUNDS = 2\nSTAGE2_WEIGHT = 0.7\nSTAGE1_WEIGHT = 0.3\n\ndef tta_predict(model, df, img_size, cache_path, n_rounds=N_TTA_ROUNDS):\n    preds_sum = None\n    for _ in range(n_rounds):\n        ds = build_dataset(df, training=False, img_size=img_size, 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\nval_true = val_split[\"label\"].values\ntarget_names = [f\"{i}: {label_map[i]}\" for i in range(NUM_CLASSES)]\n\nstart = time.time()\nval_preds_s1 = tta_predict(model_s1, val_split, STAGE1_SIZE, \"/kaggle/working/cache/val_160\")\nval_preds_s2 = tta_predict(model_s2, val_split, STAGE2_SIZE, \"/kaggle/working/cache/val_224\")\nprint(f\"TTA (both models): {time.time() - start:.1f}s\")\n\nval_preds_s2_only = np.argmax(val_preds_s2, axis=1)\nprint(\"=== Stage 2 alone ===\")\nprint(classification_report(val_true, val_preds_s2_only, target_names=target_names))\n\nval_preds_ensemble_proba = STAGE1_WEIGHT * val_preds_s1 + STAGE2_WEIGHT * val_preds_s2\nval_preds_ensemble = np.argmax(val_preds_ensemble_proba, axis=1)\nprint(\"=== Ensemble (Stage 1 + Stage 2) ===\")\nprint(classification_report(val_true, val_preds_ensemble, target_names=target_names))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:16:13.526074Z","iopub.execute_input":"2026-08-24T04:16:13.526498Z","iopub.status.idle":"2026-08-24T04:16:47.388955Z","shell.execute_reply.started":"2026-08-24T04:16:13.526473Z","shell.execute_reply":"2026-08-24T04:16:47.387916Z"}},"outputs":[],"execution_count":null},{"id":"e3fda927-e2ce-45c2-8fce-50d7aa34d1aa","cell_type":"code","source":"cm = confusion_matrix(val_true, val_preds_ensemble)\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 — Week 8 Ensemble\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:16:47.390410Z","iopub.execute_input":"2026-08-24T04:16:47.391260Z","iopub.status.idle":"2026-08-24T04:16:47.677209Z","shell.execute_reply.started":"2026-08-24T04:16:47.391216Z","shell.execute_reply":"2026-08-24T04:16:47.676261Z"}},"outputs":[],"execution_count":null},{"id":"4229f1ee-813e-4ec8-bfa0-98bd7c7d471f","cell_type":"markdown","source":"## Comparison","metadata":{}},{"id":"c7fa4827-7739-4f3b-90d2-d31469bcdf0d","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        \"EfficientNetB0 + progressive resizing + cutout (Week 7)\",\n        \"Stage 2 alone (Week 8, before ensembling)\",\n        \"Ensemble: Stage 1 (160) + Stage 2 (224) (Week 8)\"\n    ],\n    \"Accuracy\": [\n        0.5146, 0.6240, 0.6657, 0.6907, 0.7623, 0.7785,\n        accuracy_score(val_true, val_preds_s2_only),\n        accuracy_score(val_true, val_preds_ensemble)\n    ],\n    \"Macro F1\": [\n        0.2793, 0.2514, 0.5347, 0.5744, 0.5856, 0.6194,\n        f1_score(val_true, val_preds_s2_only, average=\"macro\"),\n        f1_score(val_true, val_preds_ensemble, average=\"macro\")\n    ]\n})\ncomparison","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-24T04:16:47.678238Z","iopub.execute_input":"2026-08-24T04:16:47.678562Z","iopub.status.idle":"2026-08-24T04:16:47.709145Z","shell.execute_reply.started":"2026-08-24T04:16:47.678530Z","shell.execute_reply":"2026-08-24T04:16:47.708440Z"}},"outputs":[],"execution_count":null},{"id":"f5596faf-55eb-4e5f-85fa-19f60f7b3470","cell_type":"code","source":"fig, ax = plt.subplots(figsize=(13, 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=25, 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-24T04:16:47.710020Z","iopub.execute_input":"2026-08-24T04:16:47.710355Z","iopub.status.idle":"2026-08-24T04:16:47.924197Z","shell.execute_reply.started":"2026-08-24T04:16:47.710331Z","shell.execute_reply":"2026-08-24T04:16:47.923303Z"}},"outputs":[],"execution_count":null},{"id":"c49dd4a4-83d5-43b6-8a49-61959f9edb93","cell_type":"markdown","source":"## Final submission\n\nUses the ensemble if it beat Stage 2 alone in the comparison above; otherwise submits Stage 2 alone. Check the table above before running this cell.","metadata":{}},{"id":"f2f868fd-d886-4665-bb55-d44aad4cc44a","cell_type":"code","source":"USE_ENSEMBLE_FOR_SUBMISSION = True  # set False if Stage 2 alone scored higher above\n\ntest_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\nif USE_ENSEMBLE_FOR_SUBMISSION:\n    test_preds_s1 = tta_predict(model_s1, test_df, STAGE1_SIZE, cache_path=None)\n    test_preds_s2 = tta_predict(model_s2, test_df, STAGE2_SIZE, cache_path=None)\n    test_preds_proba = STAGE1_WEIGHT * test_preds_s1 + STAGE2_WEIGHT * test_preds_s2\nelse:\n    test_preds_proba = tta_predict(model_s2, test_df, STAGE2_SIZE, cache_path=None)\n\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-24T04:16:47.925211Z","iopub.execute_input":"2026-08-24T04:16:47.925489Z","iopub.status.idle":"2026-08-24T04:17:06.797381Z","shell.execute_reply.started":"2026-08-24T04:16:47.925466Z","shell.execute_reply":"2026-08-24T04:17:06.796409Z"}},"outputs":[],"execution_count":null},{"id":"645d524d-fdb7-4048-907a-db8c3f4b97d2","cell_type":"markdown","source":"## Cleanup","metadata":{}},{"id":"3ed48ba7-c162-4180-a405-c8be69c4742b","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-24T04:17:06.799912Z","iopub.execute_input":"2026-08-24T04:17:06.800530Z","iopub.status.idle":"2026-08-24T04:17:07.569272Z","shell.execute_reply.started":"2026-08-24T04:17:06.800500Z","shell.execute_reply":"2026-08-24T04:17:07.568472Z"}},"outputs":[],"execution_count":null}]}