{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":19989,"databundleVersionId":1160143,"sourceType":"competition"},{"sourceId":6107,"sourceType":"modelInstanceVersion","modelInstanceId":4650,"modelId":2804},{"sourceId":622342,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":468144,"modelId":483974},{"sourceId":623274,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":468144,"modelId":483974}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🌾 KerasCV YOLOv8 for Global Wheat Head Detection\n\nThis notebook documents the training methodology for the **Global Wheat Detection Kaggle Competition**, addressing the critical task of accurate **object localization of wheat heads** within diverse agricultural imagery. The challenge requires robust generalization across significant variations in lighting, wheat variety, growth stage, and camera perspective.\n\n---\n\n## I. Methodology and Architecture\nThe core of this solution utilizes the **YOLOv8 object detection architecture**, implemented via the accelerated and standardized API provided by the **KerasCV library**. This choice facilitates rapid iteration while maintaining access to state-of-the-art model structures and leveraging optimized TensorFlow backends.\n\n## II. Training Strategy: Stratified Optimization\nTo ensure optimal convergence and performance, a **three-phase, stratified training regime** is employed:\n\n1.  **Warmup Phase:** A short initial training period with a controlled learning rate increase to stabilize model weights and prevent early-stage divergence.\n2.  **Mid-Tune Phase:** The primary training period, utilizing an optimized learning rate schedule (e.g., cosine decay) for comprehensive feature learning.\n3.  **Fine-Tune Phase:** A final, low-learning-rate stage dedicated to subtle refinement of weights, maximizing the model's predictive accuracy.\n\n## III. Evaluation Framework\nPerformance is measured using the standard competition metric: the **mean Average Precision (mAP)**. For accurate and compliant calculation of these metrics, we leverage the **cocometrics** library. This package provides a highly robust and widely adopted implementation of the COCO (Common Objects in Context) evaluation protocol, ensuring reliable and standard analysis of object detection performance across various Intersection over Union (IoU) thresholds.\n\n## IV. Workflow and Artifact Management\nThis notebook is strictly dedicated to the **training process**. To maintain a clean, efficient workflow and simplify the final submission environment:\n\n* **Model Checkpointing:** Successful model weights are saved at various stages throughout the training process.\n* **Artifact Export:** The final, trained model is systematically **exported and saved** to disk. This saved artifact will be loaded into a **separate, dedicated inference notebook** responsible solely for prediction generation, test-time augmentation (TTA) application, and submission file creation.\n\n---","metadata":{}},{"cell_type":"markdown","source":"## ⚙️ Section 1: Environment and Hardware Configuration\n\nThis initial section establishes the necessary environment, imports core dependencies, and performs crucial hardware checks to ensure efficient, accelerated, and reproducible model training.\n\n---\n\n### 1.1 Library Imports and System Settings\n\nThis subsection imports all necessary **libraries** for data manipulation, visualization, deep learning (**TensorFlow** and **KerasCV**), and general utility. It also configures environment variables to manage TensorFlow verbosity and warnings.","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport os\nimport sys\nimport math\nimport gc\nfrom PIL import Image\nimport cv2\nimport ast\nimport tensorflow as tf\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, TensorBoard\nimport keras_cv\nfrom sklearn.model_selection import train_test_split\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport random\nimport warnings\n# Suppress specific TensorFlow logging for a cleaner output\nwarnings.filterwarnings(\"ignore\")\nos.environ[\"TF_CPP_MIN_LOG_LEVEL\"] = \"3\"\nos.environ[\"TF_CPP_MIN_VLOG_LEVEL\"] = \"0\"\nos.environ[\"TF_ENABLE_ONEDNN_OPTS\"] = \"0\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:14:28.821215Z","iopub.execute_input":"2025-11-03T12:14:28.821395Z","iopub.status.idle":"2025-11-03T12:14:50.677953Z","shell.execute_reply.started":"2025-11-03T12:14:28.821379Z","shell.execute_reply":"2025-11-03T12:14:50.677130Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.2 Hardware Verification\n\nThis step verifies the **availability of high-performance computing devices** (GPU or TPU) within the TensorFlow environment, which is crucial for efficient execution of the deep learning pipeline.","metadata":{}},{"cell_type":"code","source":"print(\"Available devices: \\n\")\n# List and print all logical devices configured for TensorFlow\nfor device in tf.config.list_logical_devices():\n    print(device.name, device.device_type)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:14:50.681325Z","iopub.execute_input":"2025-11-03T12:14:50.681496Z","iopub.status.idle":"2025-11-03T12:14:50.898074Z","shell.execute_reply.started":"2025-11-03T12:14:50.681482Z","shell.execute_reply":"2025-11-03T12:14:50.897295Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.3 Distribution Strategy Initialization\n\nThe `get_strategy()` function implements a robust mechanism to automatically detect and configure the appropriate **TensorFlow distribution strategy**. This is critical for maximizing training speed by leveraging multiple devices (TPUs or GPUs) if available. The strategy object returned dictates how the model and data are distributed across available cores/devices.","metadata":{}},{"cell_type":"code","source":"def get_strategy():\n    \"\"\"\n    Detects and returns the best TensorFlow distribution strategy.\n    - TPUStrategy for TPU(s)\n    - MirroredStrategy for GPU(s)\n    - Default strategy for CPU\n    \"\"\"\n    try:\n        # Try TPU first: Initialize and connect to the TPU cluster\n        tpu = tf.distribute.cluster_resolver.TPUClusterResolver(tpu='local')\n        tf.config.experimental_connect_to_cluster(tpu)\n        tf.tpu.experimental.initialize_tpu_system(tpu)\n        strategy = tf.distribute.TPUStrategy(tpu)\n        print(\"Using TPU strategy:\", type(strategy).__name__)\n    except Exception:\n        # If TPU not available, try GPU\n        gpus = tf.config.list_physical_devices('GPU')\n        if gpus:\n            # Use MirroredStrategy for distributed training across multiple GPUs\n            strategy = tf.distribute.MirroredStrategy()\n            print(\"Using GPU strategy:\", type(strategy).__name__)\n        else:\n            # Fallback to CPU/default strategy\n            strategy = tf.distribute.get_strategy()\n            print(\"No TPU/GPU found. Using CPU strategy:\", type(strategy).__name__)\n\n    # Report the number of replicas, which equals the number of devices used for training\n    print(\"REPLICAS:\", strategy.num_replicas_in_sync)\n    return strategy\n\n# Execute the function to set the global distribution strategy\nstrategy = get_strategy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:14:52.385100Z","iopub.execute_input":"2025-11-03T12:14:52.385680Z","iopub.status.idle":"2025-11-03T12:14:52.737871Z","shell.execute_reply.started":"2025-11-03T12:14:52.385656Z","shell.execute_reply":"2025-11-03T12:14:52.737232Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.4 Strategy Confirmation and Version Summary\n\nThis final cell in the setup section confirms the number of **replicas** (devices) being utilized for parallel training and verifies the exact **TensorFlow version** to ensure environment consistency and reproducibility.","metadata":{}},{"cell_type":"code","source":"# Print the confirmed number of synchronous replicas (devices) being utilized\nprint(\"REPLICAS:\", strategy.num_replicas_in_sync)\n# Print the TensorFlow version for reproducibility documentation\nprint(\"TensorFlow version:\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:14:57.408492Z","iopub.execute_input":"2025-11-03T12:14:57.409251Z","iopub.status.idle":"2025-11-03T12:14:57.413647Z","shell.execute_reply.started":"2025-11-03T12:14:57.409224Z","shell.execute_reply":"2025-11-03T12:14:57.412750Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.5 Setting Global Seeds for Reproducibility\n\nTo ensure that the results from this notebook can be reliably reproduced, a fixed **random seed** is set for the core libraries involved: `random`, `numpy`, and `tensorflow`. This is essential as operations like data splitting, augmentation, and model weight initialization often involve randomness.","metadata":{}},{"cell_type":"code","source":"SEED = 28\ndef seed_everything(SEED):\n    # Set seed for the standard 'random' library\n    random.seed(SEED)\n    # Set seed for TensorFlow's global random operations\n    tf.random.set_seed(SEED)\n    # Set seed for NumPy's random operations\n    np.random.seed(SEED)\n    print('For reproducing purposes, everything seeded !')\n\n# Execute the seeding function with the defined constant\nseed_everything(SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:14:57.986038Z","iopub.execute_input":"2025-11-03T12:14:57.986790Z","iopub.status.idle":"2025-11-03T12:14:57.991590Z","shell.execute_reply.started":"2025-11-03T12:14:57.986761Z","shell.execute_reply":"2025-11-03T12:14:57.990753Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 📊 Section 2: Data Loading and Exploratory Data Analysis (EDA)\n\nThis section focuses on loading the dataset, understanding its core statistics, and visualizing key characteristics of the images and bounding box annotations. A thorough EDA is essential for making informed decisions regarding model architecture, data augmentation, and training parameters.\n\n---\n\n### 2.1 Dataset Path Configuration and Initial Counts\n\nDefine the file paths for the raw data, including the training images, test images, and the CSV file containing bounding box annotations. Initial counts of images in the respective directories and a shape check of a sample image are performed.","metadata":{}},{"cell_type":"code","source":"# Define base directory and paths for all resources\nDATA_DIR = '/kaggle/input/global-wheat-detection'\nTRAIN_DIR = os.path.join(DATA_DIR, 'train')\nTEST_DIR = os.path.join(DATA_DIR, 'test')\nCSV_PATH = os.path.join(DATA_DIR, 'train.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:03.083193Z","iopub.execute_input":"2025-11-03T12:15:03.083475Z","iopub.status.idle":"2025-11-03T12:15:03.087241Z","shell.execute_reply.started":"2025-11-03T12:15:03.083449Z","shell.execute_reply":"2025-11-03T12:15:03.086449Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count the number of files in the train and test directories\nnum_train_images = len(os.listdir(TRAIN_DIR))\nnum_test_images = len(os.listdir(TEST_DIR))\nprint(f'Number of total images on Train directory: {num_train_images}')\nprint(f'Number of test images on Test directory: {num_test_images}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:40:56.971777Z","iopub.execute_input":"2025-10-28T10:40:56.972334Z","iopub.status.idle":"2025-10-28T10:40:57.089352Z","shell.execute_reply.started":"2025-10-28T10:40:56.972309Z","shell.execute_reply":"2025-10-28T10:40:57.088662Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load a sample image to check the default image resolution\nimg_path = os.path.join(TRAIN_DIR, os.listdir(TRAIN_DIR)[0])\nimg = cv2.imread(img_path, cv2.IMREAD_COLOR)\nprint(img.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T14:44:43.929350Z","iopub.execute_input":"2025-10-29T14:44:43.930074Z","iopub.status.idle":"2025-10-29T14:44:43.970774Z","shell.execute_reply.started":"2025-10-29T14:44:43.930049Z","shell.execute_reply":"2025-10-29T14:44:43.970138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the annotation CSV file into a pandas DataFrame\ndf = pd.read_csv(CSV_PATH)\n# Display the first few rows of the DataFrame\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:05.682333Z","iopub.execute_input":"2025-11-03T12:15:05.682897Z","iopub.status.idle":"2025-11-03T12:15:05.920340Z","shell.execute_reply.started":"2025-11-03T12:15:05.682873Z","shell.execute_reply":"2025-11-03T12:15:05.919566Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.2 Annotation Summary and Bounding Box Density\n\nAnalyze the overall size of the annotation DataFrame and calculate the density of wheat heads per image to understand the scale of the object detection task. The statistics help characterize the target variance.\n\nThe key observation here is the **high variance** in the number of wheat heads per image (demonstrated by the large standard deviation and maximum value). The mean of $\\approx 43$ bounding boxes per image confirms this is a **dense detection problem**, requiring a model with strong capabilities for handling object clustering and overlap.","metadata":{}},{"cell_type":"code","source":"# Check the total number of bounding box annotations\ndf.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:04.755911Z","iopub.execute_input":"2025-10-28T10:41:04.756200Z","iopub.status.idle":"2025-10-28T10:41:04.761530Z","shell.execute_reply.started":"2025-10-28T10:41:04.756171Z","shell.execute_reply":"2025-10-28T10:41:04.760752Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate the average number of bounding boxes per image\naveraged_bbox_per_img = df.groupby('image_id').size().mean()\nprint(f'Average Bounding boxes exists in an image: {int(averaged_bbox_per_img)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:06.382384Z","iopub.execute_input":"2025-10-28T10:41:06.383111Z","iopub.status.idle":"2025-10-28T10:41:06.408530Z","shell.execute_reply.started":"2025-10-28T10:41:06.383084Z","shell.execute_reply":"2025-10-28T10:41:06.407652Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get detailed statistics on the count of wheat heads per image\nbbox_counts = df.groupby('image_id').size()\nprint('Statistics of wheat head per image:')\nprint(bbox_counts.describe().T)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:07.459364Z","iopub.execute_input":"2025-10-28T10:41:07.460049Z","iopub.status.idle":"2025-10-28T10:41:07.481895Z","shell.execute_reply.started":"2025-10-28T10:41:07.460025Z","shell.execute_reply":"2025-10-28T10:41:07.481201Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Visualize the distribution of the bounding box counts per image. The histogram visually confirms the heavy tail of the distribution, indicating a small subset of images are exceptionally dense.","metadata":{}},{"cell_type":"code","source":"plt.figure(figsize= (12, 6))\n# Create a histogram to visualize the distribution of wheat head counts\nsns.histplot(bbox_counts, bins= 30, kde= True, color= 'purple')\nplt.title('Number of Bounding Boxes per Image')\nplt.xlabel('Number of Bounding Boxes')\nplt.ylabel('Number of images')\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:09.032369Z","iopub.execute_input":"2025-10-28T10:41:09.033181Z","iopub.status.idle":"2025-10-28T10:41:09.446070Z","shell.execute_reply.started":"2025-10-28T10:41:09.033138Z","shell.execute_reply":"2025-10-28T10:41:09.445115Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.3 Analysis of Unannotated Images (Negative Samples)\n\nDetermine the proportion of images that contain no annotations (i.e., no visible wheat heads). This check is crucial to ensure the class imbalance regarding negative samples is manageable and that the model is exposed to a sufficient number of true negative examples.\n\nThe finding that **unannotated images are not dominated** (approximately 0.01 of the dataset) confirms the data distribution is relatively balanced between positive and negative training examples, which should prevent trivial prediction bias towards background.","metadata":{}},{"cell_type":"code","source":"# Identify all image IDs that have at least one annotation\nannonated_ids = set(df['image_id'].unique())\nprint(f'Number of images with Wheat: {len(annonated_ids)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:21.227929Z","iopub.execute_input":"2025-11-03T12:15:21.228253Z","iopub.status.idle":"2025-11-03T12:15:21.243208Z","shell.execute_reply.started":"2025-11-03T12:15:21.228230Z","shell.execute_reply":"2025-11-03T12:15:21.242546Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Compare all image files with annotated IDs to find empty images\nall_images = [f.replace('.jpg', '') for f in os.listdir(TRAIN_DIR)]\nempty_images = [f for f in all_images if f not in annonated_ids]\nprint(f'Number of images without annonation(Wheat): {len(empty_images)}')\nprint(f'Example of empty image: {empty_images[0]}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:21.636112Z","iopub.execute_input":"2025-11-03T12:15:21.636787Z","iopub.status.idle":"2025-11-03T12:15:21.692864Z","shell.execute_reply.started":"2025-11-03T12:15:21.636762Z","shell.execute_reply":"2025-11-03T12:15:21.692090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate the percentage of annotated and unannotated images\nempty_img_frac = len(empty_images) / len(os.listdir(TRAIN_DIR))\nannonated_img_frac = len(annonated_ids) / len(os.listdir(TRAIN_DIR))\n\nprint(f'Empty images percentage: {empty_img_frac:.4f}')\nprint(f'Annonated images percentage: {annonated_img_frac:.4f}')\nprint(\"Empty images aren't dominated, no problem with them at all!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T17:39:02.290793Z","iopub.execute_input":"2025-10-29T17:39:02.291518Z","iopub.status.idle":"2025-10-29T17:39:02.298216Z","shell.execute_reply.started":"2025-10-29T17:39:02.291491Z","shell.execute_reply":"2025-10-29T17:39:02.297485Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Display a sample of an unannotated image, confirming the appearance of a true negative example (e.g., bare soil, field edges, or non-wheat crops).","metadata":{}},{"cell_type":"code","source":"img_path = os.path.join(TRAIN_DIR, empty_images[0] + '.jpg')\nimg = Image.open(img_path)\n\nplt.imshow(img)\nplt.axis('off')\nplt.title(f'Example of empty: {empty_images[0]}.jpg')\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:27.337966Z","iopub.execute_input":"2025-10-28T10:41:27.338668Z","execution_failed":"2025-10-28T16:03:09.130Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.4 Initial Image Visualisation\n\nDefine and call a utility function to display a few random training images. This provides an initial visual inspection of the dataset's **diversity in lighting, perspective, and background clutter**, which directly influences the design of the data augmentation pipeline.","metadata":{}},{"cell_type":"code","source":"def show_images(num_images= 6, cols= 3):\n    # Determine the list of files to display\n    files = os.listdir(TRAIN_DIR)[:num_images]\n    rows = (num_images + cols - 1) // cols\n\n    fig = plt.figure(figsize= (cols* 4, rows* 4))\n    \n    for i, fname in enumerate(files):\n        img_path = os.path.join(TRAIN_DIR, fname)\n        img = Image.open(img_path)\n        img = img.resize((256, 256)) # Resize for consistent display\n\n        plt.subplot(rows, cols, i+1)\n        plt.imshow(img)\n        plt.axis('off')\n        plt.title(fname)\n        \n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:33.046573Z","iopub.execute_input":"2025-10-28T10:41:33.046834Z","iopub.status.idle":"2025-10-28T10:41:33.052249Z","shell.execute_reply.started":"2025-10-28T10:41:33.046815Z","shell.execute_reply":"2025-10-28T10:41:33.051376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display 6 sample images\nshow_images(num_images= 6, cols= 3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:41:54.773761Z","iopub.execute_input":"2025-10-28T10:41:54.774316Z","iopub.status.idle":"2025-10-28T10:41:55.667663Z","shell.execute_reply.started":"2025-10-28T10:41:54.774294Z","shell.execute_reply":"2025-10-28T10:41:55.666826Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.5 Bounding Box Transformation and Size Analysis\n\nTransform the `bbox` string column into numerical coordinates (`x_min`, `y_min`, `x_max`, `y_max`) and calculate the dimensions (`width`, `height`) of all bounding boxes. This allows for an analysis of the object scale. The coordinate system is converted from **[x_min, y_min, width, height] (PASCAL VOC format)** to **[x_min, y_min, x_max, y_max]**, which is often preferred for internal object detection calculations.","metadata":{}},{"cell_type":"code","source":"# Convert the string representation of the list in 'bbox' column to an actual list\ndf['bbox'] = df['bbox'].apply(ast.literal_eval)\n# Extract coordinates from the list: [x_min, y_min, x_max, y_max]\ndf['x_min'] = df['bbox'].apply(lambda b: b[0])\ndf['y_min'] = df['bbox'].apply(lambda b: b[1])\ndf['x_max'] = df['bbox'].apply(lambda b: b[0] + b[2])\ndf['y_max'] = df['bbox'].apply(lambda b: b[1] + b[3])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:26.814994Z","iopub.execute_input":"2025-11-03T12:15:26.815466Z","iopub.status.idle":"2025-11-03T12:15:28.452408Z","shell.execute_reply.started":"2025-11-03T12:15:26.815444Z","shell.execute_reply":"2025-11-03T12:15:28.451812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display the modified DataFrame structure\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:04.159859Z","iopub.execute_input":"2025-10-28T10:42:04.160105Z","iopub.status.idle":"2025-10-28T10:42:04.172118Z","shell.execute_reply.started":"2025-10-28T10:42:04.160088Z","shell.execute_reply":"2025-10-28T10:42:04.171516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Calculate the actual width and height of each bounding box\ndf['width'] = df['x_max'] - df['x_min']\ndf['height'] = df['y_max'] - df['y_min']\nprint(df[['width' ,'height']].describe().T)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:08.075717Z","iopub.execute_input":"2025-10-28T10:42:08.076265Z","iopub.status.idle":"2025-10-28T10:42:08.117798Z","shell.execute_reply.started":"2025-10-28T10:42:08.076241Z","shell.execute_reply":"2025-10-28T10:42:08.117159Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:09.035379Z","iopub.execute_input":"2025-10-28T10:42:09.035822Z","iopub.status.idle":"2025-10-28T10:42:09.048501Z","shell.execute_reply.started":"2025-10-28T10:42:09.035801Z","shell.execute_reply":"2025-10-28T10:42:09.047644Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Visualize the distributions of bounding box widths and heights. The statistics and plot confirm that the wheat heads are generally **small objects** (median dimensions are around 60x60 pixels in a 1024x1024 image). This small object size necessitates the use of high-resolution input and potentially multi-scale feature maps in the YOLOv8 backbone.","metadata":{}},{"cell_type":"code","source":"fig, ax = plt.subplots(1, 2, figsize= (12, 6))\nfor i, col in enumerate(['width', 'height']):\n    sns.histplot(df[col], bins= 50, kde= True, ax= ax[i])\n    ax[i].set_title(f'Bounding Boxes {col} distribution')\n    ax[i].set_xlim((0, 250))\n    ax[i].set_xlabel(f'{col} pixels')\n    ax[i].set_ylabel('Count')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:10.513238Z","iopub.execute_input":"2025-10-28T10:42:10.513834Z","iopub.status.idle":"2025-10-28T10:42:12.180119Z","shell.execute_reply.started":"2025-10-28T10:42:10.513811Z","shell.execute_reply":"2025-10-28T10:42:12.179108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def show_images_with_bboxes(df, image_dir, nrows, ncols):\n    # Pick random images from the train dir\n    files = os.listdir(image_dir)\n    selected_files = random.sample(files, nrows * ncols)\n\n    fig, axs = plt.subplots(nrows, ncols, figsize=(4*ncols, 4*nrows))\n\n    for ax, fname in zip(axs.flatten(), selected_files):\n        image_id = fname.replace('.jpg', '')\n\n        # Load image\n        img_path = os.path.join(image_dir, fname)\n        img = cv2.imread(img_path)\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        # Get bboxes if exists\n        if image_id in df['image_id'].values:\n            bboxes = df[df['image_id'] == image_id][['x_min', 'y_min', 'x_max', 'y_max']].values\n            for (x_min, y_min, x_max, y_max) in bboxes:\n                start_point = (int(x_min), int(y_min))\n                end_point = (int(x_max), int(y_max))\n                color = (255, 0, 0)\n                thickness = 2\n                cv2.rectangle(img, start_point, end_point, color, thickness)\n\n        # Show image\n        ax.imshow(img)\n        ax.axis('off')\n        ax.set_title(fname, fontsize=8)\n\n    plt.tight_layout()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:15.269223Z","iopub.execute_input":"2025-10-28T10:42:15.269719Z","iopub.status.idle":"2025-10-28T10:42:15.276568Z","shell.execute_reply.started":"2025-10-28T10:42:15.269696Z","shell.execute_reply":"2025-10-28T10:42:15.275790Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"show_images_with_bboxes(df, TRAIN_DIR, 2, 2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:42:18.278234Z","iopub.execute_input":"2025-10-28T10:42:18.278847Z","iopub.status.idle":"2025-10-28T10:42:19.771511Z","shell.execute_reply.started":"2025-10-28T10:42:18.278825Z","shell.execute_reply":"2025-10-28T10:42:19.770471Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## ⚙️ Section 3: Data Preparation and Train/Validation Split\n\nThis section transforms the raw annotation DataFrame into a format suitable for the KerasCV object detection API. It groups bounding boxes by image, structures the data into dictionary objects, performs the train/validation split, and strategically integrates the unannotated images into the training set.\n\n---\n\n### 3.1 Structuring Data for Model Input\n\nThe bounding box data is grouped by `image_id` and converted into a list of dictionaries. Each dictionary represents a single image and contains its file path and a NumPy array of all associated bounding boxes in the required **`[x_min, y_min, x_max, y_max]`** format.","metadata":{}},{"cell_type":"code","source":"# Group bounding box coordinates by image_id, resulting in a list of bboxes per image\ngrouped = df.groupby('image_id')[['x_min', 'y_min', 'x_max', 'y_max']].apply(\n    lambda x: x.values.tolist()\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:31.726457Z","iopub.execute_input":"2025-11-03T12:15:31.726731Z","iopub.status.idle":"2025-11-03T12:15:32.267260Z","shell.execute_reply.started":"2025-11-03T12:15:31.726710Z","shell.execute_reply":"2025-11-03T12:15:32.266630Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"data_dicts = []\nfor image_id, bboxes in grouped.items():\n    img_path = os.path.join(TRAIN_DIR, f'{image_id}.jpg')\n    # Convert the list of bboxes into a float32 NumPy array with shape (N, 4)\n    bboxes = np.array(bboxes, dtype=np.float32).reshape(-1, 4)\n    data_dicts .append({\n        'image_path': img_path,\n        # 'bboxes' is the key expected by KerasCV's preprocessors\n         'bboxes': bboxes\n    })","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:32.268415Z","iopub.execute_input":"2025-11-03T12:15:32.268661Z","iopub.status.idle":"2025-11-03T12:15:32.346697Z","shell.execute_reply.started":"2025-11-03T12:15:32.268635Z","shell.execute_reply":"2025-11-03T12:15:32.345951Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.2 Train-Validation Split\n\nThe dataset containing all positive samples is randomly divided into training (80%) and validation (20%) sets. The `random_state` is fixed using the global `SEED` for reproducibility.","metadata":{}},{"cell_type":"code","source":"# Split the data_dicts into train and validation sets (80/20 split)\ntrain_dicts, val_dicts = train_test_split(\n    data_dicts,\n    test_size= 0.2,\n    random_state= SEED,\n    shuffle= True\n)\nprint('Train and Validation dicts created successfully! 20% of data stored for validation')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:32.941707Z","iopub.execute_input":"2025-11-03T12:15:32.942215Z","iopub.status.idle":"2025-11-03T12:15:32.947892Z","shell.execute_reply.started":"2025-11-03T12:15:32.942190Z","shell.execute_reply":"2025-11-03T12:15:32.947085Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.3 Integration of Negative Samples\n\nThe unannotated images (`empty_images`) are explicitly added to the **training set only**. For these negative samples, the `bboxes` array is set to an empty **`(0, 4)`** array. This ensures the model learns to recognize and correctly classify true negative images, improving robustness.","metadata":{}},{"cell_type":"code","source":"for fname in empty_images:\n    img_path = os.path.join(TRAIN_DIR, f'{fname}.jpg')\n    # Create an empty bounding box array for images without wheat heads\n    bboxes = np.zeros((0, 4), dtype=np.float32)\n    train_dicts.append({\n        'image_path': img_path,\n        'bboxes': bboxes\n    })\n\n# Randomly shuffle the final training dictionary list after adding negative samples\nrandom.shuffle(train_dicts)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:33.924504Z","iopub.execute_input":"2025-11-03T12:15:33.925020Z","iopub.status.idle":"2025-11-03T12:15:33.930173Z","shell.execute_reply.started":"2025-11-03T12:15:33.924997Z","shell.execute_reply":"2025-11-03T12:15:33.929303Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🛠️ Section 4: Configuration, Data Pipeline, and Augmentation Strategy\n\nThis section establishes all essential **hyperparameters** for the YOLOv8 model and its training phases. It then defines the **input pipeline** using `tf.data` and **KerasCV layers** to efficiently load, preprocess, and augment the training and validation data. This structure ensures high throughput and effective generalization.\n\n---\n\n### 4.1 Hyperparameter Definitions\n\nKey hyperparameters governing model size, training stability, learning rate schedule, and input pipeline efficiency are defined. The use of a small number of classes (`NUM_CLASSES=1`) is due to the nature of the competition (only 'wheat head' is detected).","metadata":{}},{"cell_type":"code","source":"# --- Model and Input Configuration ---\nIMG_SIZE = (1024, 1024) # Target input resolution for the model\nNUM_CLASSES = 1 # Only one class: 'wheat head'\nGLOBAL_CLIPNORM = 10.0 # Gradient clipping value for training stability (prevents exploding gradients)\n\n# --- Learning Rate Configuration (Three-Phase Strategy) ---\nWARMUP_LR= 1e-3 # Learning rate for the initial Warmup phase\nFINE_TUNE_BB_LR = 1e-4 # Learning rate for bounding box head during Fine-Tune (if separate training is desired)\nFINE_TUNE_MODEL_LR = 1e-5 # Very low final learning rate for subtle refinement during Fine-Tune phase\n\n# --- Epoch Configuration (Three-Phase Strategy) ---\nWARMUP_EPOCH = 10 # Duration of the Warmup phase\nINTERMEDIATE_EPOCH = WARMUP_EPOCH + 20 # Total epochs up to the end of the Mid-Tune phase\nFINAL_EPOCH = INTERMEDIATE_EPOCH + 50 # Total epochs up to the end of the Fine-Tune phase\n\n# --- tf.data Pipeline Configuration ---\nAUTO = tf.data.AUTOTUNE # Optimal setting for parallel execution\nBATCH_SIZE_PER_REPLICA = 4 # Batch size allocated to each available device (GPU/TPU core)\nBUFFER_SHUFFLE_SIZE = 512 # Size of the buffer used for shuffling the dataset\n\n# Global Batch Size calculation: crucial for scaling learning rates later\nBATCH_SIZE = BATCH_SIZE_PER_REPLICA * strategy.num_replicas_in_sync \nprint(f'Global Batch size: {BATCH_SIZE}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:34.576225Z","iopub.execute_input":"2025-11-03T12:15:34.576946Z","iopub.status.idle":"2025-11-03T12:15:34.582129Z","shell.execute_reply.started":"2025-11-03T12:15:34.576921Z","shell.execute_reply":"2025-11-03T12:15:34.581407Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n### 4.2 Data Input Preparation Functions\n\nThese helper functions are designed to bridge the gap between the Python list-of-dictionaries format (`train_dicts`, `val_dicts`) and the required `tf.data.Dataset` and KerasCV formats.\n\n* `prepare_inputs`: Converts the Python list of dictionaries into a tuple of **Ragged Tensors** (`image_paths`, `classes`, `boxes`). Ragged tensors are essential here because each image has a variable number of bounding boxes.\n* `load_image`: Standard TensorFlow function to decode a JPEG file from the path.\n* `load_dataset`: Combines the loaded image tensor with the bounding boxes and classes into a dictionary, which is the standard input format for KerasCV augmentation layers.","metadata":{}},{"cell_type":"code","source":"def prepare_inputs(dicts):\n    # Convert list of image paths into a Ragged Tensor of strings\n    image_paths = tf.ragged.constant(\n        [s[\"image_path\"] for s in dicts], dtype=tf.string\n    )\n\n    bbox_list = [\n        np.array(s[\"bboxes\"], dtype=np.float32).reshape(-1, 4)\n        for s in dicts\n    ]\n\n    # Assign a class ID of 0 (since NUM_CLASSES=1, representing \"wheat head\")\n    classes_list = [\n        np.zeros((len(b)), dtype=np.float32) for b in bbox_list\n    ]\n\n    # Convert bounding box and class lists into Ragged Tensors\n    bboxes  = tf.ragged.constant(bbox_list, ragged_rank=1, dtype=tf.float32)\n    classes = tf.ragged.constant(classes_list, ragged_rank=1, dtype=tf.float32)\n\n    return image_paths, classes, bboxes\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:36.315050Z","iopub.execute_input":"2025-11-03T12:15:36.315621Z","iopub.status.idle":"2025-11-03T12:15:36.320511Z","shell.execute_reply.started":"2025-11-03T12:15:36.315598Z","shell.execute_reply":"2025-11-03T12:15:36.319729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load and decode JPEG image\ndef load_image(image_path):\n    image = tf.io.read_file(image_path)\n    image = tf.image.decode_jpeg(image, channels=3)\n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:36.404694Z","iopub.execute_input":"2025-11-03T12:15:36.404936Z","iopub.status.idle":"2025-11-03T12:15:36.408445Z","shell.execute_reply.started":"2025-11-03T12:15:36.404920Z","shell.execute_reply":"2025-11-03T12:15:36.407855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Package into the dictionary format expected by KerasCV\ndef load_dataset(image_path, classes_rt, boxes_rt):\n    image = load_image(image_path)\n    bounding_boxes = {\"boxes\": boxes_rt, \"classes\": classes_rt}\n    return {\"images\": image, \"bounding_boxes\": bounding_boxes}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:37.052627Z","iopub.execute_input":"2025-11-03T12:15:37.053514Z","iopub.status.idle":"2025-11-03T12:15:37.057623Z","shell.execute_reply.started":"2025-11-03T12:15:37.053481Z","shell.execute_reply":"2025-11-03T12:15:37.056830Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.3 Multi-Phase Data Augmentation Strategies\n\nThree distinct `tf.keras.Sequential` pipelines are defined using **KerasCV layers**. Using different augmentation strengths for different training phases is a standard technique to prevent overfitting early on (strong augmentation) and enable subtle optimization later (light augmentation).\n\n* **Strong Augmenter (Warmup/Early Mid-Tune):** Uses aggressive geometric and color transformations, including **Mosaic**, to significantly diversify the training data.\n* **Light Augmenter (Late Mid-Tune/Fine-Tune):** Reduces the magnitude of transformations, providing a more stable input distribution closer to the validation data.\n* **Validation Augmenter:** Only performs necessary deterministic operations (resizing) without any random transformations.","metadata":{}},{"cell_type":"code","source":"# Strong augmentations: Aggressive data diversification for early training\naugmenter_strong = tf.keras.Sequential([\n    # JitteredResize: Randomly scales the image before resizing to introduce scale variation\n    keras_cv.layers.JitteredResize(\n        target_size=IMG_SIZE, scale_factor=(0.9, 1.1), bounding_box_format=\"xyxy\"\n    ),\n    # Mosaic: Combines 4 images into 1, dramatically increasing batch size and context diversity\n    keras_cv.layers.Mosaic(bounding_box_format=\"xyxy\", name= 'mosaic'),\n    # Standard horizontal flipping\n    keras_cv.layers.RandomFlip(\n        mode=\"horizontal\", bounding_box_format=\"xyxy\"\n    ),\n    # Strong color distortion\n    keras_cv.layers.RandomColorJitter(\n        value_range=(0.0, 255.0),\n        brightness_factor=0.2, contrast_factor=0.2,\n        saturation_factor=0.2, hue_factor=0.1\n    ),\n    # Randomly desaturates colors (simulating sensor noise/weather)\n    keras_cv.layers.RandomColorDegeneration(\n        factor=(0.2, 0.7), seed=SEED\n    ),\n])\n\n# Light augmentations: Milder transformations for stable convergence in later phases\naugmenter_light = tf.keras.Sequential([\n    # Reduced jitter scale\n    keras_cv.layers.JitteredResize(\n        target_size=IMG_SIZE, scale_factor=(0.95, 1.05), bounding_box_format=\"xyxy\"\n    ),\n    keras_cv.layers.RandomFlip(\n        mode=\"horizontal\", bounding_box_format=\"xyxy\"\n    ),\n    # Reduced color distortion\n    keras_cv.layers.RandomColorJitter(\n        value_range=(0.0, 255.0),\n        brightness_factor=0.1, contrast_factor=0.1,\n        saturation_factor=0.1, hue_factor=0.05\n    ),\n    keras_cv.layers.RandomColorDegeneration(\n        factor=(0.1, 0.4), seed=SEED\n    ),\n])\n\n# Validation (deterministic) resizing: Only standard resizing for evaluation\naugmenter_val = tf.keras.Sequential([\n    # Fixed resize scale\n    keras_cv.layers.JitteredResize(\n        target_size=IMG_SIZE, scale_factor=(1.0, 1.0), bounding_box_format=\"xyxy\"\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:38.043811Z","iopub.execute_input":"2025-11-03T12:15:38.044523Z","iopub.status.idle":"2025-11-03T12:15:38.075361Z","shell.execute_reply.started":"2025-11-03T12:15:38.044497Z","shell.execute_reply":"2025-11-03T12:15:38.074693Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.4 Data Loading and Dataset Creation Functions\n\nThese functions orchestrate the data pipeline, applying the correct sequence of loading, batching, shuffling, and augmentation based on the desired training phase.\n\n* `dict_to_tuple`: Converts the KerasCV output dictionary back into a simple `(images, bounding_boxes)` tuple required by the Keras `model.fit()` method.\n* `create_strong_dataset`: Builds the dataset for the Warmup phase, incorporating `Mosaic` and strong color jitter. **Ragged batching** is used before augmentation, as KerasCV layers handle ragged tensors seamlessly.\n* `create_light_dataset`: Builds the dataset for the Mid-Tune and Fine-Tune phases, applying either the light training augmentations or the deterministic validation augmentations.","metadata":{}},{"cell_type":"code","source":"def dict_to_tuple(inputs):\n    # Convert KerasCV dictionary output to standard Keras (x, y) tuple\n    return inputs['images'], inputs['bounding_boxes']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:39.330878Z","iopub.execute_input":"2025-11-03T12:15:39.331561Z","iopub.status.idle":"2025-11-03T12:15:39.335088Z","shell.execute_reply.started":"2025-11-03T12:15:39.331531Z","shell.execute_reply":"2025-11-03T12:15:39.334240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_strong_dataset(dict_list, batch_size=BATCH_SIZE):\n    \n    image_paths, classes, bboxes = prepare_inputs(dict_list)\n\n    # 1. Start with tensor slices\n    ds = tf.data.Dataset.from_tensor_slices((image_paths, classes, bboxes))\n    # 2. Shuffle paths\n    ds = ds.shuffle(BUFFER_SHUFFLE_SIZE)\n    # 3. Load image from path\n    ds = ds.map(load_dataset, num_parallel_calls=AUTO)\n    # 4. Batch before augmentation (required for Mosaic and efficient augmentation)\n    ds = ds.ragged_batch(batch_size, drop_remainder=True)\n    # 5. Apply strong augmentation\n    ds = ds.map(augmenter_strong, num_parallel_calls=AUTO)\n    # 6. Convert to tuple format\n    ds = ds.map(dict_to_tuple, num_parallel_calls=AUTO)\n\n    # Pre-fetch data to overlap CPU work (loading/augmenting) with GPU work (training)\n    return ds.prefetch(AUTO)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:39.664970Z","iopub.execute_input":"2025-11-03T12:15:39.665703Z","iopub.status.idle":"2025-11-03T12:15:39.670337Z","shell.execute_reply.started":"2025-11-03T12:15:39.665676Z","shell.execute_reply":"2025-11-03T12:15:39.669604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_light_dataset(dict_list, batch_size=BATCH_SIZE, is_training= False):\n\n    image_paths, classes, bboxes = prepare_inputs(dict_list)\n    \n    ds = tf.data.Dataset.from_tensor_slices((image_paths, classes, bboxes))\n\n    # 1. Load image from path\n    ds = ds.map(load_dataset, num_parallel_calls=AUTO)\n\n    if is_training:\n        ds = ds.shuffle(BUFFER_SHUFFLE_SIZE)\n        ds = ds.ragged_batch(batch_size, drop_remainder=True)\n        # Apply light training augmentation\n        ds = ds.map(augmenter_light, num_parallel_calls=AUTO)\n    else:\n        # No shuffle for validation\n        ds = ds.ragged_batch(batch_size, drop_remainder=True)\n        # Apply deterministic validation augmentation\n        ds = ds.map(augmenter_val, num_parallel_calls=AUTO)\n    \n    # 2. Convert to tuple format\n    ds = ds.map(dict_to_tuple, num_parallel_calls=AUTO)\n    \n    # Pre-fetch for efficiency\n    return ds.prefetch(AUTO)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:41.176388Z","iopub.execute_input":"2025-11-03T12:15:41.176660Z","iopub.status.idle":"2025-11-03T12:15:41.182326Z","shell.execute_reply.started":"2025-11-03T12:15:41.176641Z","shell.execute_reply":"2025-11-03T12:15:41.181488Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.5 Final Dataset Initialization\n\nInstantiate the three required datasets based on the previously defined functions and data splits. The availability of these distinct datasets enables the implementation of the three-phase training strategy.","metadata":{}},{"cell_type":"code","source":"# Initialize the datasets for the three training phases\ntrain_strong_dataset = create_strong_dataset(train_dicts)\n# Validation set is consistent across all phases\nval_dataset = create_light_dataset(val_dicts, is_training= False)\n# Light training set for Mid-Tune and Fine-Tune\ntrain_light_dataset = create_light_dataset(train_dicts, is_training= True)\n\nprint('✅ Train and Validation datasets are ready !')\nprint('Light Augmented dataset for Mid-Tune and Fine-Tune phases created !')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:41.999792Z","iopub.execute_input":"2025-11-03T12:15:42.000546Z","iopub.status.idle":"2025-11-03T12:15:55.771367Z","shell.execute_reply.started":"2025-11-03T12:15:42.000521Z","shell.execute_reply":"2025-11-03T12:15:55.770593Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.6 Dataset Verification and Augmentation Visualization\n\nThis subsection performs two vital checks:\n1.  **Shape Verification:** Confirms that the output tensors from the `tf.data` pipeline (`train_strong_dataset`) have the expected shapes, particularly noting the ragged batching for bounding boxes.\n2.  **Visual Validation:** Defines and utilizes a utility function (`visualize_dataset`) to visually inspect the effect of the applied strong augmentations on the training data (e.g., Mosaic, color jitter) and the deterministic transformations on the validation data.\n\nThe visualization uses the convenient **KerasCV `plot_bounding_box_gallery`** function to render the ground-truth boxes on the augmented images.","metadata":{}},{"cell_type":"code","source":"# Check the shapes of the output tensors from the data pipeline\nfor images, bounding_boxes in train_strong_dataset.take(1):\n    bboxes = bounding_boxes[\"boxes\"]\n    classes = bounding_boxes[\"classes\"]\n\n    # Image shape should be (BATCH_SIZE, 1024, 1024, 3)\n    print(\"Images shape:\", images.shape)\n    # Boxes shape will be RaggedTensor, showing (BATCH_SIZE, None, 4)\n    print(\"Boxes shape:\", bboxes.shape)\n    # Classes shape will be RaggedTensor, showing (BATCH_SIZE, None)\n    print(\"Classes shape:\", classes.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:44:05.073119Z","iopub.execute_input":"2025-10-28T10:44:05.073852Z","iopub.status.idle":"2025-10-28T10:44:12.280583Z","shell.execute_reply.started":"2025-10-28T10:44:05.073826Z","shell.execute_reply":"2025-10-28T10:44:12.279926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_dataset(dataset, rows=2, cols=2, \n                      value_range=(0, 255), bounding_box_format=\"xyxy\"):\n    # Take a single batch for visualization\n    batch = next(iter(dataset.take(1)))\n    # Our dataset is already (images, bounding_boxes) from the final map function\n    images, bounding_boxes = batch\n    \n    num_images = rows * cols\n\n    # 1. Plot raw augmented images\n    fig, axs = plt.subplots(rows, cols, figsize= (4* cols, 4* rows))\n    axs = axs.flatten()\n    for i in range(num_images):\n        # Convert tensor to NumPy array for plotting\n        img = images[i].numpy().astype('uint8')\n\n        axs[i].imshow(img)\n        axs[i].set_title('Raw Image')\n        axs[i].axis('off')\n    plt.tight_layout()\n    plt.show()\n         \n    # 2. Plot images with ground-truth bounding boxes\n    keras_cv.visualization.plot_bounding_box_gallery(\n        images,                 # images\n        y_pred= bounding_boxes, # y_pred is used here as the input format is (images, y_true)\n        value_range=value_range,# range of image values\n        rows=rows,\n        cols=cols,\n        scale=5,\n        font_scale=0.7,\n        bounding_box_format=bounding_box_format,\n    )\n\n    plt.tight_layout()\n    plt.show()\n\n# Usage\nvisualize_dataset(train_strong_dataset, rows=2, cols=2)\nvisualize_dataset(val_dataset, rows=2, cols=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:47:13.160090Z","iopub.execute_input":"2025-10-28T10:47:13.160688Z","iopub.status.idle":"2025-10-28T10:47:24.781625Z","shell.execute_reply.started":"2025-10-28T10:47:13.160665Z","shell.execute_reply":"2025-10-28T10:47:24.780925Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4.7 Calculation of Training and Validation Steps\n\nDetermine the total number of batches per epoch for both the training and validation sets. These values are required inputs for the Keras `fit` method, ensuring the model iterates over the entire dataset exactly once per epoch. The `math.ceil` function is used to account for any partial final batch.","metadata":{}},{"cell_type":"code","source":"# Get the total number of samples in each split\nNUM_TRAIN_IMAGES = len(train_dicts)\nNUM_VAL_IMAGES   = len(val_dicts)\n\n# Calculate the number of batches (steps) per training epoch\nsteps_per_epoch  = math.ceil(NUM_TRAIN_IMAGES / BATCH_SIZE)\n# Calculate the number of batches (steps) for validation\nvalidation_steps = math.ceil(NUM_VAL_IMAGES / BATCH_SIZE)\n\nprint(f\"Steps per Epoch: {steps_per_epoch}\")\nprint(f\"Validation Steps: {validation_steps}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:15:55.772550Z","iopub.execute_input":"2025-11-03T12:15:55.772813Z","iopub.status.idle":"2025-11-03T12:15:55.777473Z","shell.execute_reply.started":"2025-11-03T12:15:55.772795Z","shell.execute_reply":"2025-11-03T12:15:55.776865Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧠 Section 5: YOLOv8 Model Definition and Three-Phase Training\n\nThis section defines the **YOLOv8 object detection model** using KerasCV, sets up the **optimizer and loss functions**, implements a **custom COCO metrics callback** for rigorous evaluation, and executes the three-phase training strategy: **Warmup**, **Mid-Tune**, and **Fine-Tune**.\n\n**Kaggle Artifact Management Note:**\nAll model checkpoints are saved to the `/kaggle/working/` directory in this notebook. The best performing models from each phase are subsequently uploaded and versioned as a Kaggle Model on my account for use in the separate inference notebook.I already use that to load model from this. But you are free to use it on Kaggle or Load the model on Kaggle or download it to use it. You can access these saved model versions here:  \n[Wheat Detection Models](https://www.kaggle.com/models/amirmohamadaskari/wheat-detection).\n\n---\n\n### 5.1 Model Initialization and Compilation (Phase 1 Setup)\n\nThe model is defined within the `strategy.scope()` to ensure weights are correctly initialized across all available devices (TPU/GPU cores). We utilize the **YOLOv8-M** backbone, leveraging pre-trained weights from the COCO dataset to accelerate convergence.\n\n**Key Configuration:**\n* **Backbone Freezing:** The entire backbone is initially **frozen** (`model.backbone.trainable = False`) during the Warmup phase. This stabilizes the large pre-trained weights and allows the newly initialized head/neck layers to learn effective feature representations first.\n* **Batch Normalization (BN) Freezing:** **Batch Normalization layers** are explicitly set to `trainable = False` across the entire backbone. This is a common practice when fine-tuning with a small batch size per replica (common on TPU/multi-GPU) to prevent BN running average statistics from becoming unstable.\n* **Optimizer:** The **AdamW** optimizer is chosen, featuring decoupled weight decay for better regularization.\n* **Loss Functions:** **Focal Loss** is used for classification (addressing class imbalance between background and wheat heads), and **CIoU Loss** (Complete IoU) is used for robust bounding box regression.","metadata":{}},{"cell_type":"code","source":"def create_model():\n    # Instantiate YOLOv8-M backbone with COCO pre-trained weights\n    backbone = keras_cv.models.YOLOV8Backbone.from_preset(\n        'yolo_v8_m_backbone_coco',\n        name= 'yolov8_backbone'\n    )\n    # Instantiate the full YOLOv8 Detector model\n    model = keras_cv.models.YOLOV8Detector(\n        num_classes= NUM_CLASSES,\n        bounding_box_format= 'xyxy',\n        fpn_depth= 3, # Standard Feature Pyramid Network (FPN) depth\n        backbone= backbone,\n        name= 'yolov8_detector'\n    )\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:47:54.802148Z","iopub.execute_input":"2025-10-28T10:47:54.802838Z","iopub.status.idle":"2025-10-28T10:47:54.806739Z","shell.execute_reply.started":"2025-10-28T10:47:54.802813Z","shell.execute_reply":"2025-10-28T10:47:54.806013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with strategy.scope():\n    \n    model = create_model()\n\n    # Initially freeze the backbone weights\n    model.backbone.trainable = False\n\n    # Explicitly freeze Batch Normalization layers across the backbone\n    for layer in model.backbone.layers:\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n\n    model.summary()\n    \n    # Define the AdamW optimizer with the Warmup learning rate\n    optimizer = tf.keras.optimizers.AdamW(\n    learning_rate= WARMUP_LR,\n    weight_decay= 1e-3, # Decoupled weight decay\n    beta_1= 0.9,\n    beta_2= 0.999,\n    global_clipnorm= GLOBAL_CLIPNORM) # Apply gradient clipping\n\n    classification_loss = keras_cv.losses.FocalLoss() # Used for classification\n\n    # Compile the model with specified loss functions\n    model.compile(\n        optimizer= optimizer,\n        classification_loss= classification_loss,\n        box_loss= 'ciou',\n        # Steps per execution: Increase performance on TPU/XLA by compiling a larger training graph\n        steps_per_execution= 32 if isinstance(strategy, tf.distribute.TPUStrategy) else 1\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:49:58.163431Z","iopub.execute_input":"2025-10-28T10:49:58.163776Z","iopub.status.idle":"2025-10-28T10:50:01.541902Z","shell.execute_reply.started":"2025-10-28T10:49:58.163755Z","shell.execute_reply":"2025-10-28T10:50:01.541205Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.2 Custom COCO Metrics Callback for Evaluation\n\nA custom Keras callback, `EvaluateCOCOMetricsCallback`, is implemented to calculate the competition's primary metric, **Mean Average Precision (MaP)**, at the end of each epoch.\n\nThis implementation uses `keras_cv.metrics.BoxCOCOMetrics` but manually collects all predictions and ground truths from the entire validation dataset (`self.data`) into large, single tensors. This is necessary because calling `metrics.update_state()` sequentially on batches can yield inaccurate results for global metrics like COCO MaP.\n\nThe callback also handles **saving the best model checkpoint** based on the highest achieved MaP score.","metadata":{}},{"cell_type":"code","source":"class EvaluateCOCOMetricsCallback(tf.keras.callbacks.Callback):\n    def __init__(self, data, save_path):\n        super().__init__()\n        self.data = data\n        self.metrics = keras_cv.metrics.BoxCOCOMetrics(\n            bounding_box_format=\"xyxy\",\n            evaluate_freq=1e9,  # Set to a high number as evaluation is triggered manually in on_epoch_end\n        )\n        self.save_path = save_path\n        self.best_map = -1.0 # Tracks the highest MaP achieved\n\n    def on_epoch_end(self, epoch, logs=None):\n        logs = logs or {}\n        self.metrics.reset_state()\n\n        # ---- START: MODIFIED SECTION ----\n        # 1. Create lists to hold all ground truth and prediction data\n        y_true_list = []\n        y_pred_list = []\n\n        # 2. Iterate through the entire validation dataset to collect data\n        for images, y_true in self.data:\n            # Predict on the current batch\n            y_pred = self.model.predict(images, verbose=0)\n            y_true_list.append(y_true)\n            y_pred_list.append(y_pred)\n\n        # 3. Concatenate all batches into single, large ragged tensors\n        # Concatenate ground truth (boxes and classes)\n        y_true_concat = {\n            'boxes': tf.concat([item['boxes'] for item in y_true_list], axis=0),\n            'classes': tf.concat([item['classes'] for item in y_true_list], axis=0)\n        }\n        # Concatenate predictions (boxes, classes, and confidence)\n        y_pred_concat = {\n            'boxes': tf.concat([item['boxes'] for item in y_pred_list], axis=0),\n            'classes': tf.concat([item['classes'] for item in y_pred_list], axis=0),\n            'confidence': tf.concat([item['confidence'] for item in y_pred_list], axis=0)\n        }\n        # ---- END: MODIFIED SECTION ----\n\n        # 4. Update the metric's state ONCE with the full dataset for accurate COCO metrics\n        self.metrics.update_state(y_true_concat, y_pred_concat)\n\n        # 5. Get the final results\n        metrics = self.metrics.result(force=True)\n        logs.update(metrics)\n\n        current_map = metrics[\"MaP\"]\n        \n        # Manually print the validation metrics for visibility\n        print(f\"\\nEpoch {epoch+1}: Validation Metrics\")\n        for key, value in metrics.items():\n            print(f\"  {key}: {value:.4f}\")\n\n        # Model checkpointing logic  \n        if current_map > self.best_map:\n            self.best_map = current_map\n            # Save the model to the Kaggle working directory\n            self.model.save(self.save_path)\n            print(f\"✅ Validation MaP improved to {current_map:.4f}. Model saved to {self.save_path}\")\n\n        return logs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:16:05.068404Z","iopub.execute_input":"2025-11-03T12:16:05.069030Z","iopub.status.idle":"2025-11-03T12:16:05.079007Z","shell.execute_reply.started":"2025-11-03T12:16:05.069006Z","shell.execute_reply":"2025-11-03T12:16:05.078045Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.3 Phase 1: Warmup Training\n\nThis phase trains the model for a short duration (`WARMUP_EPOCH=10`) using a relatively high, fixed learning rate (`WARMUP_LR`). The strong augmentation pipeline (`train_strong_dataset`) is used here.\n\nThe primary goal is to quickly train the uninitialized layers (head and neck) of the detector while the large pre-trained backbone remains stable (frozen). This prevents catastrophic forgetting and allows the model to find a reasonable starting point in the weight space.","metadata":{}},{"cell_type":"code","source":"# Define the save path for the best model of the Warmup phase(you can use your own path)\nphase1_saved_path = \"/kaggle/working/warmup_best_model.keras\"\n# Instantiate Callbacks\ncoco_cb = EvaluateCOCOMetricsCallback(val_dataset, \n                                      save_path= phase1_saved_path,\n                                      )\nearly_stopping_cb = EarlyStopping(\n    monitor= 'MaP',\n    patience= 3, # Stop if MaP doesn't improve for 3 epochs\n    restore_best_weights= True,\n    mode= 'max'\n)\n\nreduce_lr_cb = ReduceLROnPlateau(\n    monitor= 'MaP',\n    patience= 3,\n    factor= 0.66,\n    min_lr= WARMUP_LR * 0.1,\n    verbose= 1\n)\n\n# TensorBoard Object if you want to use\ntb_cb = TensorBoard(\n    log_dir= '/kaggle/working/logs',\n    histogram_freq= 1\n)\n\ncallbacks = [\n    coco_cb,\n    early_stopping_cb,\n    reduce_lr_cb,\n    tb_cb\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-29T14:46:20.203193Z","iopub.execute_input":"2025-10-29T14:46:20.203946Z","iopub.status.idle":"2025-10-29T14:46:20.209808Z","shell.execute_reply.started":"2025-10-29T14:46:20.203922Z","shell.execute_reply":"2025-10-29T14:46:20.209059Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"--- Starting Phase 1: Warmup Training ---\")\n# # Train the Warmup Model\nhistory = model.fit(train_strong_dataset.repeat(), \n                    validation_data= val_dataset.repeat(),\n                    epochs= WARMUP_EPOCH,\n                    callbacks= [callbacks],\n                    steps_per_epoch= steps_per_epoch,\n                    validation_steps= validation_steps)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T10:57:10.953811Z","iopub.execute_input":"2025-10-28T10:57:10.954722Z","iopub.status.idle":"2025-10-28T13:26:53.455923Z","shell.execute_reply.started":"2025-10-28T10:57:10.954695Z","shell.execute_reply":"2025-10-28T13:26:53.455298Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.4 Phase 2: Mid-Tune Training\n\nIn this phase, the training transitions from only updating the detector head to **unfreezing the lower layers of the backbone** for more specialized feature extraction.\n\n**Key Configuration:**\n* **Model Loading:** The best weights and Model saved from the Warmup phase are loaded. Note that the path `/kaggle/input/wheat-detection/keras/warmup/1/warmup_best_model.keras` points to the pre-uploaded version of the best model, ensuring a stable starting point.\n* **Layer Unfreezing:** The backbone is partially unfrozen, starting from the layer named `stack4_downsample_conv`. This selectively trains the deeper feature extraction layers.\n* **Learning Rate Schedule:** A **Cosine Decay schedule** is implemented. This schedule starts at a reasonable rate (`FINE_TUNE_BB_LR`) and smoothly decreases over the duration of the phase, allowing for fine optimization without large destructive updates.\n* **Augmentation:** The strong augmentation is replaced with the milder `train_light_dataset` to further stabilize training.","metadata":{}},{"cell_type":"code","source":"START_UNFREEZE_LAYER_NAME = 'stack4_downsample_conv'\nwith strategy.scope():\n    print(\"Loading model from warmup phase...\")\n    # Load the best model from the previous Warmup phase\n    model = tf.keras.models.load_model(\n        # The model is loaded from a pre-uploaded Kaggle Model\n        '/kaggle/input/wheat-detection/keras/warmup/1/warmup_best_model.keras',\n            custom_objects = {\n                'YOLOV8Detector': keras_cv.models.YOLOV8Detector,\n                'YOLOV8Backbone': keras_cv.models.YOLOV8Backbone\n            }\n    )\n    print(\"Model loaded successfully. Ready for Mid-Tune phase !\")\n    \n    # 1. Unfreeze the entire backbone\n    model.backbone.trainable = True\n    unfreeze_checkpoint = False\n\n    # 2. Implement partial unfreezing (freeze the first few stacks)\n    for layer in model.backbone.layers:\n        if layer.name == START_UNFREEZE_LAYER_NAME:\n            unfreeze_checkpoint = True\n        if unfreeze_checkpoint:\n            layer.trainable = True\n        else:\n            layer.trainable = False\n\n        # Ensure BN layers remain frozen regardless of backbone trainable status\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n\n    # Also check and freeze BN layers in the Neck and Head of the detector\n    for layer in model.layers:\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n    \n    # Configure Cosine Decay Learning Rate schedule\n    num_phase2_epochs = INTERMEDIATE_EPOCH - WARMUP_EPOCH\n    decay_steps = int(steps_per_epoch * num_phase2_epochs)\n    learning_rate = tf.keras.optimizers.schedules.CosineDecay(\n        initial_learning_rate=FINE_TUNE_BB_LR,\n        decay_steps=decay_steps,\n        alpha=0.1 # End LR will be 10% of initial LR (1e-5)\n    )\n\n    optimizer = tf.keras.optimizers.AdamW(\n        learning_rate = learning_rate,\n        weight_decay = 1e-4,\n        beta_1 = 0.9,\n        beta_2 = 0.999,\n        global_clipnorm = GLOBAL_CLIPNORM\n    )\n\n    classification_loss = keras_cv.losses.FocalLoss()\n    \n    # Recompile the model with the new, scheduled optimizer\n    model.compile(\n        optimizer = optimizer,\n        classification_loss = classification_loss,\n        box_loss = 'ciou',\n        steps_per_execution= 32 if isinstance(strategy, tf.distribute.TPUStrategy) else 1\n    )\n    print(\"\\n--- Model configured for Phase 2: Mid-Tune ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T16:15:27.751390Z","iopub.execute_input":"2025-10-28T16:15:27.751793Z","iopub.status.idle":"2025-10-28T16:15:32.806213Z","shell.execute_reply.started":"2025-10-28T16:15:27.751764Z","shell.execute_reply":"2025-10-28T16:15:32.805548Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define Phase 2 callbacks\nphase2_saved_path = \"/kaggle/working/midtune_best_model.keras\"\ncoco_cb = EvaluateCOCOMetricsCallback(val_dataset, \n                                       phase2_saved_path)\nearly_stopping_cb = EarlyStopping(\n    monitor= 'MaP',\n    patience= 5, # Increased patience for finer tuning\n    restore_best_weights= True,\n    mode= 'max'\n)\n\ntb_cb = TensorBoard(\n    log_dir= '/kaggle/working/logs',\n    histogram_freq= 1\n)\n\ncallbacks = [\n    coco_cb,\n    early_stopping_cb,\n    tb_cb\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T16:15:50.368791Z","iopub.execute_input":"2025-10-28T16:15:50.369283Z","iopub.status.idle":"2025-10-28T16:15:50.373931Z","shell.execute_reply.started":"2025-10-28T16:15:50.369264Z","shell.execute_reply":"2025-10-28T16:15:50.373112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"--- Starting Phase 2: Mid-Tune Training ---\")\n# Train the model for Mid-Tune, starting from WARMUP_EPOCH\nfinal_history = model.fit(\n    train_light_dataset.repeat(),\n    epochs= INTERMEDIATE_EPOCH,\n    initial_epoch= WARMUP_EPOCH,\n    validation_data= val_dataset.repeat(),\n    steps_per_epoch= steps_per_epoch,\n    validation_steps= validation_steps,\n    callbacks= callbacks\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-28T16:16:02.720937Z","iopub.execute_input":"2025-10-28T16:16:02.721237Z","iopub.status.idle":"2025-10-28T20:21:29.591894Z","shell.execute_reply.started":"2025-10-28T16:16:02.721213Z","shell.execute_reply":"2025-10-28T20:21:29.591237Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5.5 Phase 3: Fine-Tune Training\n\nThis final phase focuses on maximizing the model's performance by making small, global adjustments to all layers.\n\n**Key Configuration:**\n* **Model Loading:** The best Model from the Mid-Tune phase are loaded.that Version 2 of my Model that already give the link.\n* **Full Unfreezing:** All layers in the entire model (backbone, neck, and head) are set to **trainable**, except for the BN layers, which remain frozen.\n* **Learning Rate Schedule:** The learning rate is set to a very low value (`FINE_TUNE_MODEL_LR`) and decays further. This ensures that weight updates are minimal, allowing the model to settle into the best possible configuration without destructive jumps.\n* **Augmentation:** The mild `train_light_dataset` is maintained.","metadata":{}},{"cell_type":"code","source":"with strategy.scope():\n    print(\"Loading model from mid-tune phase...\")\n    # Load the best model from the previous Mid-Tune phase\n    model = tf.keras.models.load_model(\n        '/kaggle/input/wheat-detection/keras/warmup/2/midtune_best_model.keras',\n            custom_objects = {\n                'YOLOV8Detector': keras_cv.models.YOLOV8Detector,\n                'YOLOV8Backbone': keras_cv.models.YOLOV8Backbone\n            }\n    )\n    print(\"Model loaded successfully. Ready for Fine-Tune phase !\")\n    \n    # Set all layers in the backbone to trainable (Full Unfreezing)\n    model.backbone.trainable = True\n\n    # Ensure BN layers are frozen across the entire detector model (backbone, neck, head)\n    for layer in model.backbone.layers:\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n\n    for layer in model.layers: # Iterate through all layers of the detector model\n    # Note: We re-check for BN to catch those in the Neck and Head\n        if isinstance(layer, tf.keras.layers.BatchNormalization):\n            layer.trainable = False\n    \n    # Configure Cosine Decay Learning Rate schedule with a very low initial LR\n    num_phase3_epochs = FINAL_EPOCH - INTERMEDIATE_EPOCH\n    decay_steps = int(steps_per_epoch * num_phase3_epochs)\n    learning_rate = tf.keras.optimizers.schedules.CosineDecay(\n        initial_learning_rate=FINE_TUNE_MODEL_LR,\n        decay_steps=decay_steps,\n        alpha=0.1 # End LR will be 10% of initial LR (5e-6)\n    )\n\n    optimizer = tf.keras.optimizers.AdamW(\n        learning_rate = learning_rate,\n        weight_decay = 1e-4,\n        beta_1 = 0.9,\n        beta_2 = 0.999,\n        global_clipnorm = GLOBAL_CLIPNORM\n    )\n\n    classification_loss = keras_cv.losses.FocalLoss()\n    \n    # Recompile the model with the final optimizer settings\n    model.compile(\n        optimizer = optimizer,\n        classification_loss = classification_loss,\n        box_loss = 'ciou',\n        steps_per_execution= 32 if isinstance(strategy, tf.distribute.TPUStrategy) else 1\n    )\n    print(\"\\n--- Model configured for Phase 3: Fine-Tune ---\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:16:19.624370Z","iopub.execute_input":"2025-11-03T12:16:19.625324Z","iopub.status.idle":"2025-11-03T12:16:25.989008Z","shell.execute_reply.started":"2025-11-03T12:16:19.625295Z","shell.execute_reply":"2025-11-03T12:16:25.988197Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define Phase 3 callbacks\nphase3_saved_path = \"/kaggle/working/finetune_best_model.keras\"\ncoco_cb = EvaluateCOCOMetricsCallback(val_dataset, \n                                       phase3_saved_path)\nearly_stopping_cb = EarlyStopping(\n    monitor= 'MaP',\n    patience= 8, # Highest patience to allow for slow, subtle improvements\n    restore_best_weights= True,\n    mode= 'max'\n)\n\ntb_cb = TensorBoard(\n    log_dir= '/kaggle/working/logs',\n    histogram_freq= 1\n)\n\ncallbacks = [\n    coco_cb,\n    early_stopping_cb,\n    tb_cb\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:16:27.024352Z","iopub.execute_input":"2025-11-03T12:16:27.025274Z","iopub.status.idle":"2025-11-03T12:16:27.030137Z","shell.execute_reply.started":"2025-11-03T12:16:27.025248Z","shell.execute_reply":"2025-11-03T12:16:27.029228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"--- Starting Phase 3: Fine-Tune Training ---\")\n# Train the model, continuing from INTERMEDIATE_EPOCH, Final Model.\nfinal_history = model.fit(\n    train_light_dataset.repeat(),\n    epochs= FINAL_EPOCH,\n    initial_epoch= INTERMEDIATE_EPOCH,\n    validation_data= val_dataset.repeat(),\n    steps_per_epoch= steps_per_epoch,\n    validation_steps= validation_steps,\n    callbacks= callbacks\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-03T12:16:31.165137Z","iopub.execute_input":"2025-11-03T12:16:31.165678Z","iopub.status.idle":"2025-11-03T16:35:01.806498Z","shell.execute_reply.started":"2025-11-03T12:16:31.165657Z","shell.execute_reply":"2025-11-03T16:35:01.805889Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"---\n\n## 📈 6. Final Performance Analysis \n\nThis section provides the complete performance analysis, combining the original Sections 6.1 through 6.4.\n\n### 6.1 Phase 1: Warmup Training Summary (Epochs 1-10) 🚀\nThis phase focused on training the detector head while the pre-trained backbone was frozen.\n\n| Metric | Start (Epoch 1) | Best (Epoch 10) | Improvement |\n| :--- | :---: | :---: | :---: |\n| **Validation MaP** | 0.2530 | 0.4617 | +0.2087 |\n| MaP@[IoU=50] | 0.5888 | 0.8272 | +0.2384 |\n| Training Box Loss | 1.9803 | 1.2328 | -0.7475 |\n\n**Analysis:** Rapid and substantial convergence, establishing a strong baseline performance. The high $\\text{MaP}@[IoU=50]$ indicated the model quickly learned to locate objects.\n\n### 6.2 Phase 2: Mid-Tune Training Summary (Epochs 11-30) ⚙️\nThis phase partially unfreezing deeper backbone layers and used a Cosine Decay Learning Rate schedule.\n\n| Metric | Start (Epoch 11) | Peak (Epoch 28) | End (Epoch 30) |\n| :--- | :---: | :---: | :---: |\n| **Validation MaP** | 0.4816 | **0.5063** | 0.5052 |\n| MaP@[IoU=75] | 0.4894 | 0.5262 | 0.5246 |\n| Training Box Loss | 1.1631 | 1.0507 | 1.0480 |\n\n**Analysis:** The model crossed the $0.50$ MaP threshold and specialized, showing significant gains in $\\text{MaP}@[IoU=75]$, which translates to **higher spatial accuracy and tighter bounding boxes**. The model was well-converged for this set of trainable parameters.\n\n### 6.3 Phase 3: Fine-Tune Training Summary (Epochs 31-51) ✨\nThis phase involved fully unfreezing the entire backbone and continuing the low learning rate cosine decay schedule, aiming for marginal gains by fine-tuning all weights. (The provided logs stop at Epoch 51 out of 80).\n\n| Metric | Start (Epoch 31) | **Peak (Epoch 43)** | End (Epoch 51) | Change (Start to Peak) |\n| :--- | :---: | :---: | :---: | :---: |\n| **Validation MaP** | 0.4983 | **0.5127** | 0.5101 | **+0.0144** |\n| MaP@[IoU=50] | 0.8570 | 0.8704 | 0.8694 | +0.0134 |\n| MaP@[IoU=75] | 0.5142 | 0.5331 (E46) | 0.5285 | +0.0189 |\n| Training Box Loss | 1.0589 | 1.0081 | 0.9734 | -0.0855 |\n\n**Observations (Epochs 31-51):**\n\n* **Peak Performance:** The $\\text{MaP}$ reached its highest point yet at **$0.5127$ in Epoch 43**. This small but crucial improvement confirms that further fine-tuning all layers slightly enhanced performance.\n* **Loss Reduction:** The training loss continued its steady decline, dropping below $1.0$ (to $0.9734$ by Epoch 51).\n* **Specialization:** Noticeable improvements were seen in $\\text{MaP}@[area=small]$, indicating the deeper fine-tuning helped the model detect **tiny or distant wheat heads more effectively**.\n\n### 6.4 Final Conclusion and Contextual Evaluation\n\nBased on the three-phase training strategy, the model has achieved substantial performance growth and stability:\n\n| Phase | Epoch Range | MaP Improvement | Key Takeaway |\n| :---: | :---: | :---: | :--- |\n| **Warmup** | 1-10 | +0.2087 | Rapid baseline establishment (Head Training). |\n| **Mid-Tune** | 11-30 | +0.0446 | Bounding box refinement (Partial Backbone Fine-Tuning). |\n| **Fine-Tune** | 31-51 | +0.0144 | Final performance ceiling, improved detection of small objects (Full Backbone Fine-Tuning). |\n\nThe **best recorded $\\text{MaP}$** achieved throughout the entire training process is **$0.5127$ (Epoch 43)**. This validates the multi-stage fine-tuning strategy for adapting the large pre-trained YOLOv8 model.\n\n**Contextual Note:** Achieving better results is highly difficult due to the competition's data characteristics—the high density of small, often overlapping wheat heads. This imposes a practical ceiling on the achievable $\\text{MaP}$. Techniques like **CutMix, MixUp, or Random Erasing** could be explored in future runs to improve generalization against occlusion and scale challenges.","metadata":{}},{"cell_type":"markdown","source":"---\n\n## 💾 7. Submission Strategy & Next Steps\n\nThis section details the final plan for model deployment and submission.\n\n### 7.1 Model Saving and Checkpoints\n\nThe three best models from this training pipeline—**Warmup\\_Best\\_Model, Midtune\\_Best\\_Model, and Finetune\\_Best\\_Model**—have been saved and versioned according to their peak performance epochs.\n\nThese models are publicly available in the dedicated My Kaggle repository, which serves as the source for the submission notebook:  \n[Wheat Detection Models](https://www.kaggle.com/models/amirmohamadaskari/wheat-detection)\n\n### 7.2 Final Submission Workflow\n\nFor the final submission, the following workflow is being employed to ensure **clarity and separation of work**:\n\n1.  A **new, dedicated inference notebook** will be created, separate from the primary training script.\n2.  This notebook will load the **Final Fine-Tuned Model** (corresponding to the best $\\text{MaP}$ of $0.5127$ from Epoch 43) directly from the **Wheat Detection Models** repository.\n3.  The notebook will then apply **Test-Time Augmentation (TTA)**, which involves running the model multiple times on augmented versions of the test images and combining the results (e.g., via non-maximum suppression or weighted box fusion).\n4.  This strategic separation ensures the submission process is clean, optimized for inference speed, and clearly distinguishes the final prediction logic from the training experiments.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}