{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":13836,"databundleVersionId":1718836,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Leafguard: Cassava Disease Baseline","metadata":{}},{"cell_type":"markdown","source":"<img src=\"https://www.phipps.conservatory.org/images/made/assets/images/as_blog_image/Cover_autumn_leaves_1494_780_s_c1.jpg\">","metadata":{}},{"cell_type":"markdown","source":"## 🎯 Project Objective\n\nThe objective of this project is to build a lightweight and efficient deep learning model\nthat can automatically classify cassava leaf images into disease categories.\nThe system aims to assist early disease detection by using computer vision techniques,\nfocusing on a simple yet effective baseline suitable for real-world deployment.\n","metadata":{}},{"cell_type":"code","source":"import os, json, numpy as np, pandas as pd\nimport matplotlib.pyplot as plt\n\nimport tensorflow as tf\nfrom PIL import Image\nfrom sklearn.model_selection import StratifiedShuffleSplit\nfrom sklearn.metrics import confusion_matrix, classification_report","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:08:28.145255Z","iopub.execute_input":"2026-01-29T03:08:28.145706Z","iopub.status.idle":"2026-01-29T03:08:28.150854Z","shell.execute_reply.started":"2026-01-29T03:08:28.145669Z","shell.execute_reply":"2026-01-29T03:08:28.149679Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 📊 Exploratory Data Analysis \n\nExploratory data analysis was conducted to understand the structure and characteristics\nof the cassava leaf dataset.\n\nKey observations:\n- The dataset consists of five classes: four disease types and healthy leaves.\n- The class distribution is highly imbalanced, with *Cassava Mosaic Disease (CMD)*\n  being the dominant class.\n- Image sizes vary significantly, confirming the need for resizing and normalization.\n- Visual inspection shows noticeable texture and color differences between some classes,\n  while others are visually similar and harder to distinguish.\n\nClass distribution and sample visualizations were analyzed using bar charts,\npie charts, and example image grids.","metadata":{}},{"cell_type":"code","source":"data_path = \"/kaggle/input/cassava-leaf-disease-classification\"\ntrain_csv_path = os.path.join(data_path, \"train.csv\")\nlabel_map_path = os.path.join(data_path, \"label_num_to_disease_map.json\")\ntrain_img_path = os.path.join(data_path, \"train_images\")\ntest_img_path  = os.path.join(data_path, \"test_images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:42:48.379379Z","iopub.execute_input":"2026-01-29T02:42:48.379900Z","iopub.status.idle":"2026-01-29T02:42:48.384066Z","shell.execute_reply.started":"2026-01-29T02:42:48.379876Z","shell.execute_reply":"2026-01-29T02:42:48.383309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.read_csv(train_csv_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:42:48.385209Z","iopub.execute_input":"2026-01-29T02:42:48.385898Z","iopub.status.idle":"2026-01-29T02:42:48.440233Z","shell.execute_reply.started":"2026-01-29T02:42:48.385861Z","shell.execute_reply":"2026-01-29T02:42:48.439528Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(label_map_path, \"r\") as f:\n    label_map = json.load(f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:42:48.444187Z","iopub.execute_input":"2026-01-29T02:42:48.444646Z","iopub.status.idle":"2026-01-29T02:42:48.453794Z","shell.execute_reply.started":"2026-01-29T02:42:48.444621Z","shell.execute_reply":"2026-01-29T02:42:48.453168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[\"disease\"] = train_df[\"label\"].astype(str).map(label_map)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:42:48.454549Z","iopub.execute_input":"2026-01-29T02:42:48.454800Z","iopub.status.idle":"2026-01-29T02:42:48.476736Z","shell.execute_reply.started":"2026-01-29T02:42:48.454779Z","shell.execute_reply":"2026-01-29T02:42:48.475978Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧹 Data Processing & Preparation\n\nBefore training, the data was prepared using the following steps:\n\n- Image resizing to a fixed resolution (224×224).\n- Normalization and preprocessing compatible with MobileNetV2.\n- Stratified train–validation split to preserve class distributions.\n- Data augmentation applied only to the training set:\n  - Random horizontal flips\n  - Brightness adjustment\n  - Contrast variation\n\nA `tf.data` pipeline was used to ensure efficient loading and preprocessing during training.","metadata":{}},{"cell_type":"code","source":"train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:42:48.477647Z","iopub.execute_input":"2026-01-29T02:42:48.477920Z","iopub.status.idle":"2026-01-29T02:42:48.496266Z","shell.execute_reply.started":"2026-01-29T02:42:48.477892Z","shell.execute_reply":"2026-01-29T02:42:48.495705Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"counts = train_df[\"disease\"].value_counts()\n\nplt.figure(figsize=(10,6))\nplt.bar(counts.index, counts.values, color=\"red\")\nplt.title(\"Class Distribution\")\nplt.xlabel(\"Disease\")\nplt.ylabel(\"Count\")\nplt.xticks(rotation=90, ha=\"right\", fontsize=8)\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:43:37.439404Z","iopub.execute_input":"2026-01-29T02:43:37.440062Z","iopub.status.idle":"2026-01-29T02:43:37.606290Z","shell.execute_reply.started":"2026-01-29T02:43:37.440036Z","shell.execute_reply":"2026-01-29T02:43:37.605542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(7,7))\nplt.pie(counts.values, labels=counts.index, autopct=\"%1.1f%%\", textprops={\"fontsize\": 9})\nplt.title(\"class share\")\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:43:03.683181Z","iopub.execute_input":"2026-01-29T02:43:03.683736Z","iopub.status.idle":"2026-01-29T02:43:03.797998Z","shell.execute_reply.started":"2026-01-29T02:43:03.683707Z","shell.execute_reply":"2026-01-29T02:43:03.797268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sss = StratifiedShuffleSplit(n_splits=1, test_size=0.15, random_state=42)\ntrain_idx, val_idx = next(sss.split(train_df, train_df[\"label\"]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:44:19.218391Z","iopub.execute_input":"2026-01-29T02:44:19.219057Z","iopub.status.idle":"2026-01-29T02:44:19.234195Z","shell.execute_reply.started":"2026-01-29T02:44:19.219027Z","shell.execute_reply":"2026-01-29T02:44:19.233524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tr_df = train_df.iloc[train_idx].reset_index(drop=True)\nva_df = train_df.iloc[val_idx].reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:44:24.199933Z","iopub.execute_input":"2026-01-29T02:44:24.200255Z","iopub.status.idle":"2026-01-29T02:44:24.210004Z","shell.execute_reply.started":"2026-01-29T02:44:24.200226Z","shell.execute_reply":"2026-01-29T02:44:24.209101Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"img_size = 224\nbatch_size = 32\nnum_classes = train_df[\"label\"].nunique()\n\ndef load_img(path):\n    x = tf.io.read_file(path)\n    x = tf.image.decode_jpeg(x, channels=3)\n    x = tf.image.resize(x, (img_size, img_size))\n    x = tf.cast(x, tf.float32) / 255.0\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:44:38.174822Z","iopub.execute_input":"2026-01-29T02:44:38.175169Z","iopub.status.idle":"2026-01-29T02:44:38.180060Z","shell.execute_reply.started":"2026-01-29T02:44:38.175145Z","shell.execute_reply":"2026-01-29T02:44:38.179333Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@tf.function\ndef aug(x):\n    x = tf.image.random_flip_left_right(x)\n    x = tf.image.random_brightness(x, 0.10)\n    x = tf.image.random_contrast(x, 0.85, 1.15)\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:44:47.942713Z","iopub.execute_input":"2026-01-29T02:44:47.942988Z","iopub.status.idle":"2026-01-29T02:44:47.947861Z","shell.execute_reply.started":"2026-01-29T02:44:47.942965Z","shell.execute_reply":"2026-01-29T02:44:47.947161Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_ds(df, training=True):\n    paths = tf.constant([os.path.join(train_img_path, i) for i in df[\"image_id\"].values])\n    labels = tf.constant(df[\"label\"].values, dtype=tf.int32)\n\n    ds = tf.data.Dataset.from_tensor_slices((paths, labels))\n    if training:\n        ds = ds.shuffle(4096, seed=42, reshuffle_each_iteration=True)\n\n    def _map(p, y):\n        x = load_img(p)\n        if training:\n            x = aug(x)\n        y = tf.one_hot(y, num_classes)\n        return x, y\n\n    ds = ds.map(_map, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:44:57.627056Z","iopub.execute_input":"2026-01-29T02:44:57.627809Z","iopub.status.idle":"2026-01-29T02:44:57.633134Z","shell.execute_reply.started":"2026-01-29T02:44:57.627779Z","shell.execute_reply":"2026-01-29T02:44:57.632559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = make_ds(tr_df, training=True)\nval_ds   = make_ds(va_df, training=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:13:00.185549Z","iopub.execute_input":"2026-01-29T03:13:00.186248Z","iopub.status.idle":"2026-01-29T03:13:00.250159Z","shell.execute_reply.started":"2026-01-29T03:13:00.186220Z","shell.execute_reply":"2026-01-29T03:13:00.249608Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base = tf.keras.applications.MobileNetV2(\n    include_top=False,\n    input_shape=(img_size, img_size, 3),\n    weights=\"imagenet\")\nbase.trainable = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:45:30.088975Z","iopub.execute_input":"2026-01-29T02:45:30.089327Z","iopub.status.idle":"2026-01-29T02:45:31.953880Z","shell.execute_reply.started":"2026-01-29T02:45:30.089297Z","shell.execute_reply":"2026-01-29T02:45:31.953221Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inp = tf.keras.Input((img_size, img_size, 3))\nx = tf.keras.applications.mobilenet_v2.preprocess_input(inp * 255.0)\nx = base(x, training=False)\nx = tf.keras.layers.GlobalAveragePooling2D()(x)\nx = tf.keras.layers.Dropout(0.25)(x)\nout = tf.keras.layers.Dense(num_classes, activation=\"softmax\")(x)\n\nmodel = tf.keras.Model(inp, out)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:45:44.475280Z","iopub.execute_input":"2026-01-29T02:45:44.475638Z","iopub.status.idle":"2026-01-29T02:45:44.497635Z","shell.execute_reply.started":"2026-01-29T02:45:44.475611Z","shell.execute_reply":"2026-01-29T02:45:44.497028Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(1e-3),\n    loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.05),\n    metrics=[\"accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:45:52.102084Z","iopub.execute_input":"2026-01-29T02:45:52.102760Z","iopub.status.idle":"2026-01-29T02:45:52.116399Z","shell.execute_reply.started":"2026-01-29T02:45:52.102734Z","shell.execute_reply":"2026-01-29T02:45:52.115704Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cbs = [\n    tf.keras.callbacks.ModelCheckpoint(\"best.keras\", monitor=\"val_accuracy\", save_best_only=True, mode=\"max\"),\n    tf.keras.callbacks.ReduceLROnPlateau(monitor=\"val_loss\", factor=0.5, patience=2),\n    tf.keras.callbacks.EarlyStopping(monitor=\"val_loss\", patience=5, restore_best_weights=True),]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:45:58.798649Z","iopub.execute_input":"2026-01-29T02:45:58.798951Z","iopub.status.idle":"2026-01-29T02:45:58.803650Z","shell.execute_reply.started":"2026-01-29T02:45:58.798925Z","shell.execute_reply":"2026-01-29T02:45:58.802893Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧠 Model Architecture & Training\n\nA **MobileNetV2** architecture pre-trained on ImageNet was selected as the baseline model\ndue to its balance between performance and computational efficiency.\n\nTraining strategy:\n- Transfer learning with frozen backbone layers initially.\n- A custom classification head with global average pooling and dropout.\n- Categorical cross-entropy loss with label smoothing.\n- Adam optimizer with a learning rate scheduler.\n- Early stopping to prevent overfitting.\n\nFine-tuning was performed by unfreezing the final layers of the backbone using a lower\nlearning rate.","metadata":{}},{"cell_type":"code","source":"hist = model.fit(train_ds, validation_data=val_ds, epochs=12, callbacks=cbs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:46:15.401795Z","iopub.execute_input":"2026-01-29T02:46:15.402099Z","iopub.status.idle":"2026-01-29T02:54:25.599844Z","shell.execute_reply.started":"2026-01-29T02:46:15.402074Z","shell.execute_reply":"2026-01-29T02:54:25.598922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base.trainable = True\nfor layer in base.layers[:-30]:\n    layer.trainable = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:54:25.601563Z","iopub.execute_input":"2026-01-29T02:54:25.601915Z","iopub.status.idle":"2026-01-29T02:54:25.608911Z","shell.execute_reply.started":"2026-01-29T02:54:25.601891Z","shell.execute_reply":"2026-01-29T02:54:25.608113Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.compile(\n    optimizer=tf.keras.optimizers.Adam(2e-5),\n    loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.05),\n    metrics=[\"accuracy\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:54:25.609807Z","iopub.execute_input":"2026-01-29T02:54:25.610033Z","iopub.status.idle":"2026-01-29T02:54:25.640153Z","shell.execute_reply.started":"2026-01-29T02:54:25.610013Z","shell.execute_reply":"2026-01-29T02:54:25.639543Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"hist2 = model.fit(train_ds, validation_data=val_ds, epochs=8, callbacks=cbs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:54:25.642124Z","iopub.execute_input":"2026-01-29T02:54:25.642339Z","iopub.status.idle":"2026-01-29T02:59:42.438106Z","shell.execute_reply.started":"2026-01-29T02:54:25.642319Z","shell.execute_reply":"2026-01-29T02:59:42.437218Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_hist(h, title):\n    plt.figure(figsize=(10,4))\n    plt.plot(h.history[\"loss\"], label=\"train loss\")\n    plt.plot(h.history[\"val_loss\"], label=\"val loss\")\n    plt.title(f\"{title} - loss\")\n    plt.xlabel(\"epoch\")\n    plt.legend()\n    plt.tight_layout()\n    plt.show()\n\n    plt.figure(figsize=(10,4))\n    plt.plot(h.history[\"accuracy\"], label=\"train acc\")\n    plt.plot(h.history[\"val_accuracy\"], label=\"val acc\")\n    plt.title(f\"{title} - accuracy\")\n    plt.xlabel(\"epoch\")\n    plt.legend()\n    plt.tight_layout()\n    plt.show()\n\nplot_hist(hist, \"stage 1\")\nplot_hist(hist2, \"stage 2\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T02:59:42.439433Z","iopub.execute_input":"2026-01-29T02:59:42.439777Z","iopub.status.idle":"2026-01-29T02:59:43.102695Z","shell.execute_reply.started":"2026-01-29T02:59:42.439751Z","shell.execute_reply":"2026-01-29T02:59:43.101983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df = pd.read_csv(os.path.join(data_path, \"sample_submission.csv\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:00:28.161400Z","iopub.execute_input":"2026-01-29T03:00:28.162217Z","iopub.status.idle":"2026-01-29T03:00:28.175454Z","shell.execute_reply.started":"2026-01-29T03:00:28.162175Z","shell.execute_reply":"2026-01-29T03:00:28.174600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:00:31.063801Z","iopub.execute_input":"2026-01-29T03:00:31.064085Z","iopub.status.idle":"2026-01-29T03:00:31.071892Z","shell.execute_reply.started":"2026-01-29T03:00:31.064061Z","shell.execute_reply":"2026-01-29T03:00:31.071074Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def make_test_ds(df):\n    paths = tf.constant([os.path.join(test_img_path, i) for i in df[\"image_id\"].values])\n\n    ds = tf.data.Dataset.from_tensor_slices(paths)\n\n    def _map(p):\n        x = load_img(p)          \n        return x\n\n    ds = ds.map(_map, num_parallel_calls=tf.data.AUTOTUNE)\n    ds = ds.batch(batch_size).prefetch(tf.data.AUTOTUNE)\n    return ds\n\ntest_ds = make_test_ds(sub_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:00:34.365138Z","iopub.execute_input":"2026-01-29T03:00:34.365902Z","iopub.status.idle":"2026-01-29T03:00:34.414063Z","shell.execute_reply.started":"2026-01-29T03:00:34.365871Z","shell.execute_reply":"2026-01-29T03:00:34.413526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_prob = model.predict(test_ds, verbose=0)\ntest_pred = np.argmax(test_prob, axis=1)\n\nsub_df[\"label\"] = test_pred.astype(int)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:12:50.068684Z","iopub.execute_input":"2026-01-29T03:12:50.069338Z","iopub.status.idle":"2026-01-29T03:12:50.106788Z","shell.execute_reply.started":"2026-01-29T03:12:50.069310Z","shell.execute_reply":"2026-01-29T03:12:50.106151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:00:48.384550Z","iopub.execute_input":"2026-01-29T03:00:48.385242Z","iopub.status.idle":"2026-01-29T03:00:48.392278Z","shell.execute_reply.started":"2026-01-29T03:00:48.385215Z","shell.execute_reply":"2026-01-29T03:00:48.391577Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sub_df.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:00:48.393289Z","iopub.execute_input":"2026-01-29T03:00:48.393677Z","iopub.status.idle":"2026-01-29T03:00:48.408616Z","shell.execute_reply.started":"2026-01-29T03:00:48.393649Z","shell.execute_reply":"2026-01-29T03:00:48.408089Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\n\ndata_path = \"/kaggle/input/cassava-leaf-disease-classification\"\nsrc = os.path.join(data_path, \"label_num_to_disease_map.json\")\ndst = \"/kaggle/working/label_num_to_disease_map.json\"\nshutil.copy(src, dst)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-29T03:07:24.866970Z","iopub.execute_input":"2026-01-29T03:07:24.867770Z","iopub.status.idle":"2026-01-29T03:07:24.876344Z","shell.execute_reply.started":"2026-01-29T03:07:24.867741Z","shell.execute_reply":"2026-01-29T03:07:24.875759Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🏆 Best Model Performance\n\nThe final model achieved stable and consistent performance on the validation set.\n\nKey results:\n- Validation accuracy converged around the low-to-mid 70% range.\n- No significant overfitting was observed.\n- The model performed best on the majority class (CMD), with lower recall on minority classes,\n  which is expected due to dataset imbalance.\n\nThe trained model was exported and used for single-image inference in a Streamlit application,\ndemonstrating its usability in a real-time prediction scenario.","metadata":{}}]}