{"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":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CSIRO Image2Biomass Prediction with Lightning ⚡\n\n---\n\n### 1. Introduction 🌿\n\nThis notebook presents an end-to-end solution for the **CSIRO - Image2Biomass Prediction** competition, which focuses on solving an image-based **regression** task. The goal is to predict the **biomass value** of pasture from field images.\n\nThe implementation leverages key PyTorch ecosystem tools:\n* **PyTorch Lightning (Lit):** For structuring the training pipeline and model logic.\n* **TIMM (PyTorch Image Models):** For easy access to and use of modern, pre-trained vision models.\n\n---\n\n### 2. Key Features of Data Handling 🖼️\n\nData management is handled using custom **`BiomassDataset`** and a **`BiomassDataModule`** (a standard PyTorch Lightning practice), which reveals a critical, non-standard feature of the competition data structure.\n\n#### Data Pipeline Details\n* **Data Module:** The **`BiomassDataModule`** handles the loading of the CSV, creating a random **train/validation split** (e.g., 80/20 split), and generating efficient PyTorch `DataLoader` objects.\n* **Augmentations (Training Transforms):** Robust data augmentation is applied to the training set to prevent overfitting:\n    * **Spatial:** **`RandomResizedCrop`** (to 224x224), **`RandomHorizontalFlip`**, **`RandomVerticalFlip`**, and **`RandomRotation`**.\n    * **Color:** **`ColorJitter`** for saturation adjustment.\n    * **Standardization:** Conversion to tensor and **ImageNet-standard normalization** is applied to all splits.\n\n---\n\n### 3. Model and Training 🤖\n\n#### Model Architecture\nThe solution uses a modern, pre-trained network adapted for regression.\n\n* **Backbone:** A **Vision Transformer (ViT)** architecture is employed, sourced from the **TIMM** library.\n* **Adaptation:** The model's classification head is replaced with a **single linear layer** to output the continuous biomass prediction, thereby adapting the image classification backbone for the regression task. \n\n#### Training Configuration\nThe training workflow is managed by PyTorch Lightning and includes crucial components:\n* **Task Type:** Image **Regression**.\n* **Loss Function:** The model is trained using a loss suitable for regression (e.g., **Mean Squared Error (MSE)**).\n* **Callbacks:** Training stability and optimization are managed via PyTorch Lightning **Callbacks**:\n    * **`ModelCheckpoint`:** Saves the model weights that yield the best performance on the validation set.\n    * **`EarlyStopping`:** Stops training automatically if the validation metric does not improve after a set number of epochs.\n    * **`LearningRateMonitor`:** Tracks and logs the optimizer's learning rate throughout training.\n\n---\n\nCompetition: https://www.kaggle.com/competitions/csiro-biomass\n\nAuthor: Based on https://github.com/Borda/kaggle_image-classify","metadata":{}},{"cell_type":"code","source":"# !pip install -q pytorch-lightning torchmetrics timm","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:17.864380Z","iopub.execute_input":"2025-10-31T15:14:17.864653Z","iopub.status.idle":"2025-10-31T15:14:17.869880Z","shell.execute_reply.started":"2025-10-31T15:14:17.864627Z","shell.execute_reply":"2025-10-31T15:14:17.868960Z"},"_kg_hide-output":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport random\nfrom typing import Optional\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport timm\nfrom timm.data import resolve_data_config\nfrom timm.data.transforms_factory import create_transform\n\nimport pytorch_lightning as pl\nimport torchmetrics\nimport warnings\n\nwarnings.simplefilter(action='ignore', category=FutureWarning)\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"Lightning: {pl.__version__}\")\nprint(f\"TIMM: {timm.__version__}\")\nprint(f\"Device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU'}\")\n\npl.seed_everything(42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:17.870640Z","iopub.execute_input":"2025-10-31T15:14:17.870890Z","iopub.status.idle":"2025-10-31T15:14:33.770778Z","shell.execute_reply.started":"2025-10-31T15:14:17.870865Z","shell.execute_reply":"2025-10-31T15:14:33.770171Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Explore Data","metadata":{}},{"cell_type":"code","source":"PATH_DATA = '/kaggle/input/csiro-biomass'\nPATH_TRAIN_CSV = os.path.join(PATH_DATA, 'train.csv')\nPATH_TRAIN_IMG = os.path.join(PATH_DATA, 'train')\nPATH_TEST_IMG = os.path.join(PATH_DATA, 'test')\n\ndf = pd.read_csv(PATH_TRAIN_CSV)\nprint(f\"Dataset size: {df.shape}\")\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:33.772166Z","iopub.execute_input":"2025-10-31T15:14:33.772577Z","iopub.status.idle":"2025-10-31T15:14:33.808816Z","shell.execute_reply.started":"2025-10-31T15:14:33.772559Z","shell.execute_reply":"2025-10-31T15:14:33.808299Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TARGET_COLS = [c for c in df.columns if c not in ['image_id', 'Image']]\nprint(f\"Target columns: {TARGET_COLS}\")\nprint(f\"Number of targets: {len(TARGET_COLS)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:33.809519Z","iopub.execute_input":"2025-10-31T15:14:33.809777Z","iopub.status.idle":"2025-10-31T15:14:33.814120Z","shell.execute_reply.started":"2025-10-31T15:14:33.809754Z","shell.execute_reply":"2025-10-31T15:14:33.813559Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Plot target distribution","metadata":{}},{"cell_type":"code","source":"# Exclude non-numeric or identifier columns from histogram plotting\ncols_to_plot = [col for col in TARGET_COLS if col not in ['sample_id', 'image_path', 'State', 'target_name']]\n\nfor col in cols_to_plot:\n    plt.figure(figsize=(8, 3)) # Create a new figure for each histogram\n    plt.hist(df[col].dropna(), bins=50, edgecolor='black', alpha=0.7)\n    plt.xlabel(col, fontsize=12)\n    plt.ylabel('Count', fontsize=12)\n    plt.title(f'{col} Distribution', fontsize=14, fontweight='bold')\n    plt.grid(alpha=0.3)\n    plt.xticks(rotation=45, ha=\"right\") # Rotate x-axis labels\n    plt.tight_layout() # Adjust layout to prevent overlap\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:33.814757Z","iopub.execute_input":"2025-10-31T15:14:33.814982Z","iopub.status.idle":"2025-10-31T15:14:35.011113Z","shell.execute_reply.started":"2025-10-31T15:14:33.814967Z","shell.execute_reply":"2025-10-31T15:14:35.010479Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cols_to_plot = ['State', 'target_name']\nn_rows, n_cols = 1, len(cols_to_plot)\nfig, axes = plt.subplots(n_rows, n_cols, figsize=(4 * n_cols, 4 * n_rows))\n\n# Ensure axes is an array even for a single subplot\naxes = axes.flatten()\n\nfor ax, col in zip(axes, cols_to_plot):\n    counts = df[col].value_counts()\n    ax.pie(counts, labels=counts.index, autopct='%1.1f%%', startangle=140)\n    ax.set_title(f'Distribution of {col}', fontsize=14, fontweight='bold')\n    ax.axis('equal')  # Equal aspect ratio ensures that pie is drawn as a circle.\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:35.011859Z","iopub.execute_input":"2025-10-31T15:14:35.012048Z","iopub.status.idle":"2025-10-31T15:14:35.195773Z","shell.execute_reply.started":"2025-10-31T15:14:35.012034Z","shell.execute_reply":"2025-10-31T15:14:35.195000Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert 'Sampling_Date' to datetime objects\ndf['Sampling_Date'] = pd.to_datetime(df['Sampling_Date'])\n\n# Extract the day of the year\ndf['Day_of_Year'] = df['Sampling_Date'].dt.dayofyear\n\n# Calculate the correlation between 'target' and 'Day_of_Year'\ncorrelation = df['target'].corr(df['Day_of_Year'])\n\nprint(f\"The correlation between 'target' and 'Day_of_Year' is: {correlation}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:35.196377Z","iopub.execute_input":"2025-10-31T15:14:35.196594Z","iopub.status.idle":"2025-10-31T15:14:35.211558Z","shell.execute_reply.started":"2025-10-31T15:14:35.196579Z","shell.execute_reply":"2025-10-31T15:14:35.210887Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Show sample images","metadata":{}},{"cell_type":"code","source":"def show_images(df_sample, n=12, path_img=PATH_DATA):\n    \"\"\"Displays a linear sampling of images sorted by target value.\"\"\"\n\n    # Sort the DataFrame by the 'target' column\n    df_sorted = df_sample.sort_values(by='target').reset_index(drop=True)\n\n    # Perform linear sampling\n    indices_to_show = np.linspace(0, len(df_sorted) - 1, n, dtype=int)\n    df_to_show = df_sorted.iloc[indices_to_show]\n\n    # Determine the number of rows and columns for subplots\n    n_cols = 3  # You can adjust this number\n    n_rows = (n + n_cols - 1) // n_cols\n\n    fig, axes = plt.subplots(n_rows, n_cols, figsize=(16, 4 * n_rows))\n    axes = axes.flatten()\n\n    # Remove unused subplots if any\n    for i in range(n, len(axes)):\n        fig.delaxes(axes[i])\n\n    for i, (idx, row) in enumerate(df_to_show.iterrows()):\n        # Use image_path directly (includes train/ID....jpg)\n        img_path = os.path.join(path_img, row['image_path'])\n\n        if os.path.exists(img_path):\n            img = Image.open(img_path).convert('RGB')\n            axes[i].imshow(img)\n            # Include the target value in the title\n            title = f\"ID: {row['sample_id']}\\nTarget: {row['target']:.2f}\"\n            axes[i].set_title(title, fontsize=10)\n        axes[i].axis('off')\n\n    plt.tight_layout()\n    plt.show()\n\n# Example usage: Show 12 images linearly sampled based on target value\nshow_images(df, n=12)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:35.212384Z","iopub.execute_input":"2025-10-31T15:14:35.213012Z","iopub.status.idle":"2025-10-31T15:14:40.224494Z","shell.execute_reply.started":"2025-10-31T15:14:35.212989Z","shell.execute_reply":"2025-10-31T15:14:40.223324Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfrom PIL import Image\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\n# Collect image sizes\nimage_sizes = []\nimage_dir = os.path.join(PATH_DATA, 'train') # Assuming 'train' directory contains images\n\nfor img_file in os.listdir(image_dir):\n    img_path = os.path.join(image_dir, img_file)\n    with Image.open(img_path) as img:\n        image_sizes.append({'width': img.width, 'height': img.height})\n\nimage_sizes_df = pd.DataFrame(image_sizes)\n\n# Create a scatter plot and histograms for image dimensions\nfig = plt.figure(figsize=(10, 10))\ngs = fig.add_gridspec(2, 2, width_ratios=(4, 1), height_ratios=(3, 1), # Adjusted height_ratios\n                      left=0.1, right=0.9, bottom=0.1, top=0.9,\n                      wspace=0.05, hspace=0.05)\n\nax_scatter = fig.add_subplot(gs[0, 0])\nax_histy = fig.add_subplot(gs[0, 1]) # Removed sharey\nax_histx = fig.add_subplot(gs[1, 0]) # Removed sharex\n\n# Scatter plot\nsns.scatterplot(data=image_sizes_df, x='width', y='height', ax=ax_scatter, alpha=0.6, s=10)\nax_scatter.set_xlabel('Width')\nax_scatter.set_ylabel('Height')\nax_scatter.set_title('Image Dimensions Scatter Plot')\nax_scatter.grid(True, alpha=0.3) # Add grid to scatter plot\nax_scatter.set_xlim(0, 3000) # Adjusted x-axis limits to include 2000\nax_scatter.set_ylim(0, 2500) # Adjusted y-axis limits to include 1000\n\n# Histograms\nsns.histplot(data=image_sizes_df, x='width', ax=ax_histx, bins=1, kde=False) # Adjusted bins for single value\nax_histx.set_title('Width Distribution')\nax_histx.set_xlabel('Width')\nax_histx.set_xlim(0, 3000) # Adjusted x-axis limits to include 2000\n\nsns.histplot(data=image_sizes_df, y='height', ax=ax_histy, bins=1, kde=False) # Adjusted bins for single value\nax_histy.set_title('Height Distribution')\nax_histy.set_ylabel('Height')\nax_histy.set_ylim(0, 2500) # Adjusted y-axis limits to include 1000\n\n\nplt.suptitle('Image Dimension Analysis', fontsize=16, fontweight='bold', y=1.02)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:40.230028Z","iopub.execute_input":"2025-10-31T15:14:40.230401Z","iopub.status.idle":"2025-10-31T15:14:44.123623Z","shell.execute_reply.started":"2025-10-31T15:14:40.230368Z","shell.execute_reply":"2025-10-31T15:14:44.122833Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset & DataModule\n\nAs you noted, the `BiomassDataset` further augments the data by splitting each image into left and right halves. This strategy serves several purposes:\n\n1.  **Enhanced Data Augmentation:** Beyond the expansion from multi-target samples in the CSV, splitting each image into two distinct halves (left and right) effectively doubles the dataset again. This provides the model with more diverse training examples, which is crucial for improving generalization and preventing overfitting, especially in scenarios with a relatively small number of unique physical images.\n\n2.  **Spatial Feature Learning:** Even though a single biomass target applies to the entire original image, different halves might present distinct visual characteristics or localized biomass distribution patterns. By treating each half as an independent sample, the model is compelled to extract relevant features from both sides, leading to a more comprehensive understanding of the image content pertinent to biomass prediction.\n\n3.  **Adaptation to CNN Input Requirements:** The competition images can often have wide aspect ratios. Many convolutional neural networks (CNNs), especially those pre-trained on ImageNet, are optimized for square or near-square input dimensions. Splitting a wide image into two halves naturally creates two narrower, more manageable rectangular inputs. These inputs are then subjected to further transformations like resizing and cropping (`transforms.RandomResizedCrop` or `transforms.CenterCrop` in `BiomassDataModule`) to fit the specific square input size expected by the model's backbone (e.g., 576x576).\n\nThis combined approach of expanding `(image_id, target_type)` pairs from the CSV and then physically splitting images into halves for input to the CNN is a robust method to maximize data utility and improve model performance.","metadata":{}},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    \"\"\"Simple dataset for biomass regression.\"\"\"\n\n    def __init__(self, df, path_img, transforms=None, mode='train'):\n        self.df = df.reset_index(drop=True)\n        self.path_img = path_img\n        self.transforms = transforms\n        self.mode = mode\n        # Assume target column exists for train mode, but not necessarily for test\n        self.target_col = 'target' if mode == 'train' else None\n        # Each image will be split into two for both train and test modes\n        self._len = len(self.df) * 2\n\n\n    def __len__(self):\n        return self._len\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx // 2] # Adjust index for both train and test mode\n        img_relative_path = row['image_path']\n        img_path = os.path.join(self.path_img, img_relative_path)\n        img = Image.open(img_path).convert('RGB')\n\n        # Determine which half to load for both train and test mode\n        half = idx % 2 # 0 for the first half, 1 for the second half\n        width, height = img.size\n        if half == 0:\n            # First half (left side)\n            img_cropped = img.crop((0, 0, width // 2, height))\n        else:\n            # Second half (right side)\n            img_cropped = img.crop((width // 2, 0, width, height))\n\n\n        if self.transforms:\n            img_cropped = self.transforms(img_cropped)\n\n        if self.mode == 'test':\n            # For test mode, return image and the image_path\n            return img_cropped, img_relative_path\n\n        # For train mode, return image and target\n        target = torch.tensor(row[self.target_col], dtype=torch.float32)\n        return img_cropped, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:44.124420Z","iopub.execute_input":"2025-10-31T15:14:44.124993Z","iopub.status.idle":"2025-10-31T15:14:44.131735Z","shell.execute_reply.started":"2025-10-31T15:14:44.124969Z","shell.execute_reply":"2025-10-31T15:14:44.130798Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Initialize the dataset (assuming you have a DataFrame 'df' and image path 'PATH_DATA')\ndataset = BiomassDataset(df, PATH_DATA)\n\n# Get three random indices\nrandom_indices = random.sample(range(len(dataset)), 3)\n\n# Display the random samples\nfig, axes = plt.subplots(1, 3, figsize=(15, 5))\n\nfor i, idx in enumerate(random_indices):\n    img, target = dataset[idx]\n    # Convert the PyTorch tensor image back to PIL Image for displaying\n    # This assumes the default tensor format from PILToTensor or similar\n    if isinstance(img, torch.Tensor):\n        img = img.permute(1, 2, 0).numpy() # Assuming CxHxW format, convert to HxWxD\n\n    axes[i].imshow(img)\n    axes[i].set_title(f\"Target: {target}\") # Display targets\n    # axes[i].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:44.132644Z","iopub.execute_input":"2025-10-31T15:14:44.132934Z","iopub.status.idle":"2025-10-31T15:14:45.277476Z","shell.execute_reply.started":"2025-10-31T15:14:44.132917Z","shell.execute_reply":"2025-10-31T15:14:45.276239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchvision import transforms\nfrom pathlib import Path\n\n\nclass BiomassDataModule(pl.LightningDataModule):\n    \"\"\"Simple DataModule for biomass regression.\"\"\"\n\n    def __init__(self, data_path, batch_size=32, img_size=(456, 456), val_split=0.2):\n        super().__init__()\n        self.data_path = data_path\n        self.batch_size = batch_size\n        self.img_size = img_size\n        self.val_split = val_split\n        self.train_df: Optional[pd.DataFrame] = None\n        self.val_df: Optional[pd.DataFrame] = None\n        self.test_df: Optional[pd.DataFrame] = None  # Add test_df\n        # ImageNet standards\n        self._color_mean = [0.485, 0.456, 0.406]\n        self._color_std = [0.229, 0.224, 0.225]\n        inimg_size = int(img_size[0] * 1.5)\n        # Define the transforms\n        self.transforms = transforms.Compose([\n            transforms.Resize((inimg_size, inimg_size)),\n            transforms.RandomResizedCrop(self.img_size), # Add random resized crop\n            transforms.RandomHorizontalFlip(), # Add random horizontal flip\n            transforms.RandomVerticalFlip(), # Add random vertical flip\n            #transforms.RandomRotation(degrees=15), # Add random rotation\n            transforms.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.1), # Add random color jitter with more parameters\n            transforms.RandomAffine(degrees=15, translate=(0.1, 0.1), scale=(0.9, 1.1)), # Add random affine transformations\n            transforms.GaussianBlur(kernel_size=3), # Add Gaussian blur\n            transforms.ToTensor(),\n            transforms.Normalize(mean=self._color_mean, std=self._color_std),\n            # Note: Adding noise directly in transforms.Compose can be tricky with torchvision\n            # For simple noise, you might add it as a custom transform or after ToTensor\n            # Example of adding simple Gaussian noise after ToTensor:\n            # lambda x: x + torch.randn_like(x) * 0.01 # Add Gaussian noise with small std\n        ])\n        self.test_transforms = transforms.Compose([\n            transforms.Resize((inimg_size, inimg_size)),\n            transforms.CenterCrop(self.img_size), # Add random resized crop\n            transforms.ToTensor(),\n            transforms.Normalize(mean=self._color_mean, std=self._color_std),\n        ])\n        # Automatically determine image path and target columns\n        self.df = pd.read_csv(os.path.join(self.data_path, 'train.csv'))\n        self._num_workers = os.cpu_count() if os.cpu_count() is not None else 0\n\n\n    def setup(self, stage: Optional[str] = None):\n        # Shuffle and split the DataFrame manually\n        if stage == 'fit' or stage is None:\n            shuffled_df = self.df.sample(frac=1, random_state=42).reset_index(drop=True)\n            val_size = int(len(shuffled_df) * self.val_split)\n            self.train_df = shuffled_df[:-val_size]\n            self.val_df = shuffled_df[-val_size:]\n\n        if stage == 'test' or stage is None:\n            # For test data, we need to create a DataFrame from the image file paths\n            test_image_dir = os.path.join(self.data_path, 'test')\n            assert os.path.isdir(test_image_dir)\n            test_image_paths = glob.glob(os.path.join(test_image_dir, '*.jpg'))\n            # Extract sample_id from the image paths (assuming filename is sample_id.jpg)\n            test_data = [{'sample_id': Path(p).stem, 'image_path': os.path.relpath(p, self.data_path)} for p in test_image_paths]\n            self.test_df = pd.DataFrame(test_data)\n\n    def train_dataloader(self):\n        train_dataset = BiomassDataset(self.train_df, self.data_path, transforms=self.transforms, mode='train')\n        return DataLoader(train_dataset, batch_size=self.batch_size, shuffle=True, num_workers=self._num_workers)\n\n    def val_dataloader(self):\n        val_dataset = BiomassDataset(self.val_df, self.data_path, transforms=self.test_transforms, mode='train') # Use test_transforms for validation\n        return DataLoader(val_dataset, batch_size=self.batch_size, shuffle=False, num_workers=self._num_workers)\n\n    def test_dataloader(self):\n        if self.test_df is None:\n            self.setup(stage='test') # Ensure test_df is loaded\n\n        test_dataset = BiomassDataset(self.test_df, self.data_path, transforms=self.test_transforms, mode='test')\n        return DataLoader(test_dataset, batch_size=self.batch_size, shuffle=False, num_workers=0) # Set num_workers to 0 for testing","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:45.278647Z","iopub.execute_input":"2025-10-31T15:14:45.278880Z","iopub.status.idle":"2025-10-31T15:14:45.294508Z","shell.execute_reply.started":"2025-10-31T15:14:45.278863Z","shell.execute_reply":"2025-10-31T15:14:45.293585Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Example Usage (assuming df, PATH_DATA and TARGET_COLS are defined)\ndata_module = BiomassDataModule(PATH_DATA, batch_size=8, img_size=(576, 576))\ndata_module.setup()\n\n# You can now access the dataloaders\ntrain_loader = data_module.train_dataloader()\nval_loader = data_module.val_dataloader()\n\nprint(f\"Number of training batches: {len(train_loader)}\")\nprint(f\"Number of validation batches: {len(val_loader)}\")\n\n# Example of getting a batch (optional)\ntrain_images, train_targets = next(iter(train_loader))\nprint(f\"Shape of training images batch: {train_images.shape}\")\nprint(f\"Shape of training targets batch: {train_targets.shape}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:45.295410Z","iopub.execute_input":"2025-10-31T15:14:45.295752Z","iopub.status.idle":"2025-10-31T15:14:48.236274Z","shell.execute_reply.started":"2025-10-31T15:14:45.295736Z","shell.execute_reply":"2025-10-31T15:14:48.235503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get the first batch from the training dataloader\ntrain_images, train_targets = next(iter(train_loader))\n\n# Determine how many images to show (e.g., the first 4 from the batch)\nn_images_to_show = min(4, train_images.shape[0])\n\nfig, axes = plt.subplots(1, n_images_to_show, figsize=(4 * n_images_to_show, 5))\n\n# Ensure axes is an array even for a single image\nif n_images_to_show == 1:\n    axes = [axes]\n\nfor i in range(n_images_to_show):\n    img = train_images[i].permute(1, 2, 0).numpy() # Convert from CxHxW to HxWxD for displaying\n    # Denormalize the image for better visualization (using ImageNet standards)\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    img = std * img + mean\n    img = np.clip(img, 0, 1) # Clip values to be between 0 and 1\n\n    axes[i].imshow(img)\n    # Assuming targets are a list of values for each image\n    axes[i].set_title(f\"Target: {train_targets[i].tolist()}\")\n    #axes[i].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:48.237633Z","iopub.execute_input":"2025-10-31T15:14:48.237942Z","iopub.status.idle":"2025-10-31T15:14:52.533175Z","shell.execute_reply.started":"2025-10-31T15:14:48.237910Z","shell.execute_reply":"2025-10-31T15:14:52.532089Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# LightningModule & training\n\n\nThe `BiomassRegressionModel` is a PyTorch Lightning module designed for the biomass regression task. It leverages a pre-trained image classification backbone and adapts it with a custom regression head to predict continuous biomass values.\n\n### 1. Backbone Architecture\n*   **Source:** The model utilizes a pre-trained image model from the `timm` (PyTorch Image Models) library. This allows for transfer learning, where the model benefits from features learned on large datasets like ImageNet.\n*   **Configuration:** The backbone is initialized with `num_classes=0` to remove its original classification head, making it suitable for feature extraction. The `global_pool='avg'` argument ensures that features are pooled into a fixed-size vector, regardless of the input image dimensions, which is then fed to the regression head.\n\n### 2. Regression Head\nTo move beyond a simple linear mapping from features to a single output, a more complex, configurable regression head has been implemented:\n*   **Flexible Multi-Layer Perceptron (MLP):** Instead of a single linear layer, the regression head is now an MLP that can consist of one or more hidden layers. The number and size of these layers are determined by the `hidden_dims` parameter, which is a list of integers.\n*   **Layer Structure:** For each dimension specified in `hidden_dims`:\n    *   A `nn.Linear` layer transforms the input features to the specified `hidden_dim`.\n    *   An `nn.ReLU` activation function introduces non-linearity.\n    *   A `nn.Dropout` layer is applied for regularization, helping to prevent overfitting by randomly setting a fraction of input units to zero during training. The dropout rate is controlled by `dropout_rate`.\n*   **Output Layer:** The final layer is a `nn.Linear` layer that maps the output of the last hidden layer to the `num_targets` (which is typically 1 for a single biomass prediction).\n\n### 3. Loss Functions\nThe model uses a combined loss function to provide a robust training signal:\n*   **`nn.SmoothL1Loss` (Huber Loss):** This loss function is less sensitive to outliers than MSELoss and is a good choice for regression tasks, especially when large errors are possible. Its contribution is weighted by `loss_weight_smooth_l1`.\n*   **`nn.MSELoss` (Mean Squared Error Loss):** A standard loss function for regression, it penalizes larger errors more significantly. Its contribution is weighted by `(1 - loss_weight_smooth_l1)`.\n*   **Combined Loss:** The final loss is a weighted sum of `SmoothL1Loss` and `MSELoss`, allowing for a balance between robustness to outliers and sensitivity to large errors.\n\n### 4. Metrics\nDuring training and validation, the model tracks key regression metrics:\n*   **Mean Absolute Error (MAE):** Measures the average magnitude of the errors, without considering their direction. It is a robust metric that is less sensitive to outliers.\n*   **Mean Squared Error (MSE):** Measures the average of the squares of the errors. It penalizes larger errors more heavily than MAE.\n\n### 5. Optimizer and Learning Rate Scheduler\n*   **Optimizer:** The model uses `torch.optim.AdamW`, an adaptive learning rate optimization algorithm that combines the benefits of Adam with weight decay regularization.\n*   **Learning Rate Scheduler:** A `ReduceLROnPlateau` scheduler is employed. This scheduler reduces the learning rate when a metric (e.g., `val_loss`) has stopped improving for a certain number of epochs (`patience`), helping the model to converge more effectively in later stages of training.\n\n---\n\nto select backbones: https://github.com/huggingface/pytorch-image-models/blob/main/results/results-imagenet-a-clean.csv","metadata":{}},{"cell_type":"code","source":"import torch.nn as nn\nimport timm\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau, CosineAnnealingLR\n\n\nclass BiomassRegressionModel(pl.LightningModule):\n    \"\"\"Simple regression model for biomass.\"\"\"\n\n    def __init__(\n        self,\n        model_name=\"tf_efficientnetv2_m\",\n        pretrained=True,\n        num_targets=1,\n        learning_rate=5e-4,\n        loss_weight_smooth_l1=0.5,\n        T_max=10,\n        hidden_dims=[256, 128],  # Changed to hidden_dims (list) for multiple layers\n        dropout_rate=0.5\n    ):\n        super().__init__()\n        self.save_hyperparameters()\n\n        # Load a pre-trained transformer model from timm\n        # num_classes=0 removes the original classifier head\n        self.backbone = timm.create_model(\n            model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n\n        # Build a more complex regression head with variable hidden layers\n        layers = []\n        in_features = self.backbone.num_features\n        for h_dim in hidden_dims:\n            layers.append(nn.Linear(in_features, h_dim))\n            layers.append(nn.ReLU())\n            layers.append(nn.Dropout(p=dropout_rate))\n            in_features = h_dim\n        layers.append(nn.Linear(in_features, num_targets))\n        self.regression_head = nn.Sequential(*layers)\n\n        # Loss functions\n        self.smooth_l1_criterion = nn.SmoothL1Loss()\n        self.mse_criterion = nn.MSELoss()\n\n        # Metrics\n        self.train_mae = torchmetrics.MeanAbsoluteError()\n        self.val_mae = torchmetrics.MeanAbsoluteError()\n        self.train_mse = torchmetrics.MeanSquaredError()\n        self.val_mse = torchmetrics.MeanSquaredError()\n\n    def forward(self, x):\n        # Pass the input through the backbone\n        features = self.backbone(x)\n        # Pass the features through the regression head\n        output = self.regression_head(features)\n        return output\n\n    def _compute_loss(self, outputs, targets):\n        \"\"\"Computes the combined loss.\"\"\"\n        smooth_l1_loss = self.smooth_l1_criterion(outputs.squeeze(), targets.squeeze())\n        mse_loss = self.mse_criterion(outputs.squeeze(), targets.squeeze())\n        loss = self.hparams.loss_weight_smooth_l1 * smooth_l1_loss + (1 - self.hparams.loss_weight_smooth_l1) * mse_loss\n        return loss, smooth_l1_loss, mse_loss\n\n\n    def training_step(self, batch, batch_idx):\n        images, targets = batch\n        outputs = self(images)\n        loss, smooth_l1_loss, mse_loss = self._compute_loss(outputs, targets)\n\n        self.train_mae(outputs.squeeze(), targets.squeeze())\n        self.train_mse(outputs.squeeze(), targets.squeeze())\n\n        self.log('train_smooth_l1_loss', smooth_l1_loss, on_step=True, prog_bar=False)\n        self.log('train_mse_loss', mse_loss, on_step=True, prog_bar=False)\n        self.log('train_loss', loss, on_step=True, prog_bar=True)\n        self.log('train_mae', self.train_mae, on_step=True, prog_bar=True)\n        self.log('train_mse', self.train_mse, on_step=True, prog_bar=False)\n        return loss\n\n    def validation_step(self, batch, batch_idx):\n        images, targets = batch\n        outputs = self(images)\n        loss, smooth_l1_loss, mse_loss = self._compute_loss(outputs, targets)\n\n        self.val_mae(outputs.squeeze(), targets.squeeze())\n        self.val_mse(outputs.squeeze(), targets.squeeze())\n\n        self.log('val_smooth_l1_loss', smooth_l1_loss, on_step=False, prog_bar=False)\n        self.log('val_mse_loss', mse_loss, on_step=False, prog_bar=False)\n        self.log('val_loss', loss, on_step=True, prog_bar=False)\n        self.log('val_mae', self.val_mae, on_step=True, prog_bar=True)\n        self.log('val_mse', self.val_mse, on_step=True, prog_bar=False)\n        return loss\n\n\n    def predict_step(self, batch, batch_idx):\n        \"\"\"Prediction step for the test set.\"\"\"\n        images, sample_path = batch\n        outputs = self(images)\n        # Return predictions and image path\n        return outputs.squeeze(), sample_path\n\n\n    def configure_optimizers(self):\n        # Use AdamW optimizer\n        optimizer = torch.optim.AdamW(self.parameters(), lr=self.hparams.learning_rate) # Use AdamW\n        # Configure the learning rate scheduler\n        scheduler = {\n            'scheduler': ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=3),\n            'interval': 'epoch',\n            'monitor': 'val_loss'\n        }\n        return [optimizer], [scheduler]\n\n\n# Example of initializing the model\nmodel = BiomassRegressionModel(\n    model_name=\"efficientnet_h_b5\", pretrained=False, learning_rate=1e-3)\n# print(model)","metadata":{"trusted":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:52.534652Z","iopub.execute_input":"2025-10-31T15:14:52.535402Z","iopub.status.idle":"2025-10-31T15:14:53.246172Z","shell.execute_reply.started":"2025-10-31T15:14:52.535371Z","shell.execute_reply":"2025-10-31T15:14:53.245600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pytorch_lightning.loggers import CSVLogger\n\n# Initialize the CSVLogger\nlogger = CSVLogger(\"logs\", name=\"biomass_regression\")\n\n# Initialize the Trainer\ntrainer = pl.Trainer(\n    max_epochs=25, # You can adjust the number of epochs\n    logger=logger,\n    accelerator='auto', # Use auto to automatically select accelerator (GPU/CPU)\n    devices='auto', # Use auto to automatically select devices\n    precision='16-mixed', # Use Automatic Mixed Precision (AMP)\n    log_every_n_steps=5, # Update progress bar every 5 steps\n    gradient_clip_val=1.0, # Add gradient clipping to prevent NaN\n    accumulate_grad_batches=6,\n)\n\n# Fit the model\ntrainer.fit(model, data_module)","metadata":{"trusted":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-10-31T15:16:45.323127Z","iopub.execute_input":"2025-10-31T15:16:45.323445Z","execution_failed":"2025-10-31T15:19:37.239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the path to save the model\nmodel_save_path = \"biomass_regression_model.pth\"\n\n# Save the model's state dictionary\ntorch.save(model.state_dict(), model_save_path)\nprint(f\"Model saved to {model_save_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.253540Z","iopub.status.idle":"2025-10-31T15:14:53.253761Z","shell.execute_reply.started":"2025-10-31T15:14:53.253658Z","shell.execute_reply":"2025-10-31T15:14:53.253667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set()\n\n# Read the metrics.csv using the trainer's logger directory\nmetrics = pd.read_csv(f\"{trainer.logger.log_dir}/metrics.csv\")\n\n# Remove the step column and set epoch as index\n# metrics.set_index(\"step\", inplace=True)\ndisplay(metrics.dropna(axis=1, how=\"all\").head())\n\n# Melt the DataFrame to long-form for plotting\nmetrics_melted = metrics.reset_index().melt(id_vars='epoch', var_name='metric', value_name='value')\n\n# Define metric groups\nmetric_groups = {\n    'Loss': [c for c in metrics.columns if \"_loss\" in c],\n    'MAE': [c for c in metrics.columns if \"_mae\" in c and \"loss\" not in c],\n    'MSE': [c for c in metrics.columns if \"_mse\" in c and \"loss\" not in c],\n}\n\n# Plot metrics for each group in a separate chart\nfor title, metric_list in metric_groups.items():\n    # Filter melted DataFrame for the current group\n    group_metrics = metrics_melted[metrics_melted['metric'].isin(metric_list)]\n\n    plt.figure(figsize=(10, 5))\n    sns.lineplot(data=group_metrics, x='epoch', y='value', hue='metric')\n    plt.title(f'{title} over Epochs', fontsize=14, fontweight='bold')\n    plt.xlabel('Epoch', fontsize=12)\n    plt.ylabel(title, fontsize=12)\n    plt.grid(True, alpha=0.3)\n    plt.yscale('log')  # Set y-axis to logarithmic scale\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.255093Z","iopub.status.idle":"2025-10-31T15:14:53.255746Z","shell.execute_reply.started":"2025-10-31T15:14:53.255571Z","shell.execute_reply":"2025-10-31T15:14:53.255587Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Prediction & Submission\n\nAfter the model has been trained, the next step is to generate predictions on the unseen test dataset and format them for submission.\n\n### 1. Test Data Loading and Visualization\n*   The `BiomassDataModule` is used to load the test images without associated targets (as shown in cell `4MH3O0SRI9vF`).\n*   Similar to training data, a few sample test images can be visualized to ensure correct loading and preprocessing.\n\n### 2. Generating Predictions\n*   The trained `BiomassRegressionModel` is put into evaluation mode (`model.eval()`), and gradients are disabled (`torch.no_grad()`) for efficient inference.\n*   The test `DataLoader` is iterated through, and for each batch, the model predicts biomass values for the test images (see cell `q7TdgWO3I9vF`).\n*   The predictions and corresponding image paths are collected.\n\n### 3. Post-processing Predictions\n*   **Negative Value Handling:** Any negative predictions (which are physically impossible for biomass) are clipped to zero.\n*   **Averaging Duplicates:** Since the `BiomassDataset` splits each image into two halves, there will be two predictions per original test image. These are grouped by `image_path` and averaged to get a single prediction per image (`prediction_df` in cell `wBABJKZVI9vF`).\n\n### 4. Creating the Submission File\n*   The final averaged predictions are merged with the original `test.csv` file, ensuring that each `sample_id` has its corresponding predicted `target` value.\n*   The resulting DataFrame, containing `sample_id` and `target`, is then saved to a `submission.csv` file in the format required by the competition (as shown in cells `sqUFOedXI9vF` and `B2zRwxhrI9vF`).","metadata":{}},{"cell_type":"code","source":"# Assuming data_module is already initialized and setup has been called for the test stage\n# If not, uncomment and run the DataModule initialization and setup cells first\n# data_module = BiomassDataModule(PATH_DATA, batch_size=64)\n# data_module.setup(stage='test')\n\n# Get the test dataloader\ntest_loader = data_module.test_dataloader()\n\n# Get the first batch from the test dataloader\ntest_images, test_sample_ids = next(iter(test_loader))\n\n# Determine how many images to show (e.g., the first 4 from the batch)\nn_images_to_show = min(4, test_images.shape[0])\n\nfig, axes = plt.subplots(1, n_images_to_show, figsize=(4 * n_images_to_show, 5))\n\n# Ensure axes is an array even for a single image\nif n_images_to_show == 1:\n    axes = [axes]\n\nfor i in range(n_images_to_show):\n    img = test_images[i].permute(1, 2, 0).numpy() # Convert from CxHxW to HxWxD for displaying\n    # Denormalize the image for better visualization (using ImageNet standards)\n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    img = std * img + mean\n    img = np.clip(img, 0, 1) # Clip values to be between 0 and 1\n\n    axes[i].imshow(img)\n    # Display the sample_id for test images\n    axes[i].set_title(test_sample_ids[i])\n    axes[i].axis('off')\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.256841Z","iopub.status.idle":"2025-10-31T15:14:53.257265Z","shell.execute_reply.started":"2025-10-31T15:14:53.257137Z","shell.execute_reply":"2025-10-31T15:14:53.257152Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Need to re-initialize the test dataloader to get the sample_ids in order\ntest_loader = data_module.test_dataloader()\n\n# Debugging: Print the type and content of test_loader\nprint(f\"Type of test_loader: {type(test_loader)}\")\nprint(f\"Content of test_loader: {test_loader}\")\n\n\n# Generate predictions on the test set by manually iterating through the dataloader\nall_predictions = []\nall_image_paths = []\n\nmodel.eval() # Set the model to evaluation mode\nwith torch.no_grad(): # Disable gradient calculation\n    for images, img_path in test_loader:\n        # Move images to the same device as the model\n        images = images.to(model.device)\n        outputs = model(images)\n        # Convert outputs to a list of values\n        outputs = outputs.squeeze().tolist()\n        if not isinstance(outputs, list):\n            # edge case with just single image\n            outputs = [outputs]\n        all_predictions.extend(outputs) # Flatten and convert to list\n        all_image_paths.extend(img_path)\n\n\n# Create a DataFrame with predictions and image paths\npredictions_raw_df = pd.DataFrame({'image_path': all_image_paths, 'target': all_predictions})\n\n# Display the first few rows of the submisshttps://cdn.prod.website-files.com/680a070c3b99253410dd3dcf/69009d67bee1b2807736006e_0637_One_Republic_September_2025_METTY_%20copy.jpgion DataFrame\nprint(\"Submission DataFrame head:\")\ndisplay(predictions_raw_df.head())\n\n# You can save the submission_df to a CSV file in the required format\n# submission_df.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.258373Z","iopub.status.idle":"2025-10-31T15:14:53.258665Z","shell.execute_reply.started":"2025-10-31T15:14:53.258548Z","shell.execute_reply":"2025-10-31T15:14:53.258561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# prevent any negative values\npredictions_raw_df[predictions_raw_df[\"target\"] < 0][\"target\"] = 0\n\n# Group by image_path and take the mean of the predictions\nprediction_df = predictions_raw_df.groupby('image_path')['target'].mean().reset_index()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.259359Z","iopub.status.idle":"2025-10-31T15:14:53.259620Z","shell.execute_reply.started":"2025-10-31T15:14:53.259495Z","shell.execute_reply":"2025-10-31T15:14:53.259509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define the path to the sample submission file\ntest_csv_path = os.path.join(PATH_DATA, 'test.csv')\n\n# Load the sample submission file\ntest_csv = pd.read_csv(test_csv_path)\n# display(test_csv.head())\n\n# del sample_submission_df['target']\ntest_csv = test_csv.merge(prediction_df, on='image_path', how='left')\ndisplay(test_csv.head())\n\n# dump prediction into CSV file\ntest_csv[[\"sample_id\", \"target\"]].to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.260758Z","iopub.status.idle":"2025-10-31T15:14:53.261040Z","shell.execute_reply.started":"2025-10-31T15:14:53.260921Z","shell.execute_reply":"2025-10-31T15:14:53.260934Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! head submission.csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-31T15:14:53.262197Z","iopub.status.idle":"2025-10-31T15:14:53.262507Z","shell.execute_reply.started":"2025-10-31T15:14:53.262354Z","shell.execute_reply":"2025-10-31T15:14:53.262367Z"}},"outputs":[],"execution_count":null}]}