{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":14482773,"sourceType":"datasetVersion","datasetId":9250318}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Train Biomass Model","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nimport warnings\n\n# Suppress warnings for cleaner output\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T07:42:08.196994Z","iopub.execute_input":"2026-01-13T07:42:08.197265Z","iopub.status.idle":"2026-01-13T07:42:18.763862Z","shell.execute_reply.started":"2026-01-13T07:42:08.197237Z","shell.execute_reply":"2026-01-13T07:42:18.763274Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T07:42:29.704893Z","iopub.execute_input":"2026-01-13T07:42:29.705230Z","iopub.status.idle":"2026-01-13T07:42:30.033228Z","shell.execute_reply.started":"2026-01-13T07:42:29.705208Z","shell.execute_reply":"2026-01-13T07:42:30.032177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Configuration ---\nBASE_DIR = os.getcwd()\n#CSV_PATH = os.path.join(BASE_DIR, 'train.csv')\nCSV_PATH = '/kaggle/input/csiro-biomass/train.csv'\nIMAGE_DIR = '/kaggle/input/csiro-biomass' #BASE_DIR  # CSV image_path contains 'train/', so we use base dir\nBATCH_SIZE = 16\nLEARNING_RATE = 0.001\nNUM_EPOCHS = 30\nTARGET_COLS = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nprint(f\"Using device: {DEVICE}\")\nprint(f\"Base Directory: {BASE_DIR}\")\nfor dirname, _, filenames in os.walk('/kaggle'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T08:14:51.574477Z","iopub.execute_input":"2026-01-13T08:14:51.574818Z","iopub.status.idle":"2026-01-13T08:14:51.803915Z","shell.execute_reply.started":"2026-01-13T08:14:51.574792Z","shell.execute_reply":"2026-01-13T08:14:51.803137Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Data Preparation ---\ndef load_and_process_data(csv_path):\n    print(f\"Loading data from {csv_path}...\")\n    df = pd.read_csv(csv_path)\n    \n    # The CSV has multiple rows per image (one for each target type).\n    # We need to extract the unique image ID to pivot the table.\n    # image_path format: train/ID1011485656.jpg -> ID: ID1011485656\n    df['image_id'] = df['image_path'].apply(lambda x: os.path.basename(x).split('.')[0])\n    \n    # Pivot the table so each image is one row with columns for each target\n    pivot_df = df.pivot_table(index='image_id', columns='target_name', values='target').reset_index()\n    pivot_df.fillna(0.0, inplace=True) # Ensure no NaNs if targets are missing\n    \n    # Extract metadata (State, Species, etc. are constant for an image_id)\n    meta_cols = ['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\n    meta_df = df.groupby('image_id')[meta_cols].first().reset_index()\n    \n    # Merge metadata with targets\n    data = pd.merge(meta_df, pivot_df, on='image_id')\n    \n    # Feature Engineering: Extract Month from Date\n    data['Sampling_Date'] = pd.to_datetime(data['Sampling_Date'])\n    data['Month'] = data['Sampling_Date'].dt.month\n    \n    # Handle potential missing values in numerical columns\n    data['Pre_GSHH_NDVI'] = data['Pre_GSHH_NDVI'].fillna(data['Pre_GSHH_NDVI'].mean())\n    data['Height_Ave_cm'] = data['Height_Ave_cm'].fillna(data['Height_Ave_cm'].mean())\n    \n    print(f\"Processed dataframe shape: {data.shape}\")\n    print(f\"Total unique images found: {len(data)}\")\n    return data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T07:42:58.229102Z","iopub.execute_input":"2026-01-13T07:42:58.229383Z","iopub.status.idle":"2026-01-13T07:42:58.236133Z","shell.execute_reply.started":"2026-01-13T07:42:58.229361Z","shell.execute_reply":"2026-01-13T07:42:58.235279Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Dataset Class ---\nclass BiomassDataset(Dataset):\n    def __init__(self, dataframe, root_dir, transform=None, meta_scaler=None, one_hot_encoder=None, is_train=True):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.transform = transform\n        self.is_train = is_train\n        \n        # Numerical features\n        self.num_features = dataframe[['Pre_GSHH_NDVI', 'Height_Ave_cm', 'Month']].values.astype(np.float32)\n        \n        # Categorical features\n        cat_features = dataframe[['State', 'Species']].values\n        \n        # Encoders and Scalers\n        if is_train:\n            self.ohe = OneHotEncoder(handle_unknown='ignore', sparse_output=False)\n            self.ohe_features = self.ohe.fit_transform(cat_features)\n            \n            self.scaler = StandardScaler()\n            self.features = np.hstack([self.num_features, self.ohe_features])\n            self.features = self.scaler.fit_transform(self.features)\n        else:\n            self.ohe = one_hot_encoder\n            self.ohe_features = self.ohe.transform(cat_features)\n            \n            self.scaler = meta_scaler\n            self.features = np.hstack([self.num_features, self.ohe_features])\n            self.features = self.scaler.transform(self.features)\n            \n        self.targets = dataframe[TARGET_COLS].values.astype(np.float32)\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, idx):\n        img_name = self.dataframe.iloc[idx]['image_path']\n        img_path = os.path.join(self.root_dir, img_name)\n        \n        # Load Image\n        try:\n            image = Image.open(img_path).convert('RGB')\n        except (FileNotFoundError, OSError):\n            # Fallback for missing images during development/testing\n            image = Image.new('RGB', (224, 224))\n\n        if self.transform:\n            image = self.transform(image)\n            \n        features = torch.tensor(self.features[idx], dtype=torch.float32)\n        target = torch.tensor(self.targets[idx], dtype=torch.float32)\n        \n        return image, features, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T08:21:09.430101Z","iopub.execute_input":"2026-01-13T08:21:09.430384Z","iopub.status.idle":"2026-01-13T08:21:09.439249Z","shell.execute_reply.started":"2026-01-13T08:21:09.430364Z","shell.execute_reply":"2026-01-13T08:21:09.438117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Model Architecture ---\nclass BiomassPredictor(nn.Module):\n    def __init__(self, num_meta_features, num_targets):\n        super(BiomassPredictor, self).__init__()\n\n        super(BiomassPredictor, self).__init__()\n        WEIGHTS_PATH = '/kaggle/input/pytorch-biomass/resnet18.pth'\n        # 1. Initialize model without downloading weights\n        self.cnn = models.resnet18(weights=None)\n        \n        # 2. Load the weights from the local Kaggle input directory\n        if os.path.exists(WEIGHTS_PATH):\n            state_dict = torch.load(WEIGHTS_PATH, map_location=DEVICE, weights_only=False)\n            self.cnn.load_state_dict(state_dict)\n            print(\"Successfully loaded weights.\")\n            print(\"Successfully loaded weights from local path.\")\n        else:\n            print(\"Weight file not found. Check your dataset path!\")\n\n        # 3. Replace the final layer as you were doing before\n        self.cnn.fc = nn.Identity()\n        \n        # Image Branch: Pre-trained ResNet18\n        # Using weights='DEFAULT' loads the best available weights (ImageNet)\n       # self.cnn = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)\n        \n        # Replace the final fully connected layer with Identity to get raw features (512 dim)\n       # self.cnn.fc = nn.Identity()\n        \n        # Metadata Branch: Simple MLP\n        self.meta_fc = nn.Sequential(\n            nn.Linear(num_meta_features, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n        \n        # Combined Branch\n        # 512 (ResNet) + 32 (Meta) = 544\n        self.final_fc = nn.Sequential(\n            nn.Linear(512 + 32, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_targets) # Regression output\n        )\n        \n    def forward(self, image, meta):\n        x_img = self.cnn(image)\n        x_meta = self.meta_fc(meta)\n        \n        # Concatenate image and metadata features\n        x = torch.cat((x_img, x_meta), dim=1)\n        x = self.final_fc(x)\n        return x # Return (Batch, Num_Targets)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T08:39:11.915865Z","iopub.execute_input":"2026-01-13T08:39:11.916154Z","iopub.status.idle":"2026-01-13T08:39:11.923903Z","shell.execute_reply.started":"2026-01-13T08:39:11.916135Z","shell.execute_reply":"2026-01-13T08:39:11.922879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Main Execution ---\n\n# 1. Load Data\nprint(CSV_PATH) \nfull_df = load_and_process_data(CSV_PATH)\n\n# 2. Split Data\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, random_state=42)\n\n# 3. Transforms\ntrain_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\nval_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\n# 4. Create Datasets & DataLoaders\ntrain_dataset = BiomassDataset(train_df, IMAGE_DIR, transform=train_transforms, is_train=True)\n# Pass fitted scaler/encoder to validation set\nval_dataset = BiomassDataset(val_df, IMAGE_DIR, transform=val_transforms, \n                             meta_scaler=train_dataset.scaler, \n                             one_hot_encoder=train_dataset.ohe, is_train=False)\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\n# 5. Initialize Model\nnum_meta_features = train_dataset.features.shape[1]\nmodel = BiomassPredictor(num_meta_features, num_targets=len(TARGET_COLS)).to(DEVICE)\n\ncriterion = nn.MSELoss()\noptimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)\n\n# 6. Training Loop\nprint(\"Starting training...\")\nfor epoch in range(NUM_EPOCHS):\n    model.train()\n    running_loss = 0.0\n    \n    for images, meta, targets in train_loader:\n        images, meta, targets = images.to(DEVICE), meta.to(DEVICE), targets.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = model(images, meta)\n        loss = criterion(outputs, targets)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    epoch_loss = running_loss / len(train_loader.dataset)\n    \n    # Validation\n    model.eval()\n    val_loss = 0.0\n    with torch.no_grad():\n        for images, meta, targets in val_loader:\n            images, meta, targets = images.to(DEVICE), meta.to(DEVICE), targets.to(DEVICE)\n            outputs = model(images, meta)\n            loss = criterion(outputs, targets)\n            val_loss += loss.item() * images.size(0)\n            \n    val_loss = val_loss / len(val_loader.dataset)\n    \n    print(f\"Epoch {epoch+1}/{NUM_EPOCHS} - Train Loss (MSE): {epoch_loss:.4f} - Val Loss (MSE): {val_loss:.4f} - RMSE: {np.sqrt(val_loss):.4f}\")\n\nprint(\"Training complete.\")\n\n# Save model\ntorch.save(model.state_dict(), os.path.join(BASE_DIR, 'biomass_model.pth'))\nprint(f\"Model saved to {os.path.join(BASE_DIR, 'biomass_model.pth')}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T08:39:20.575621Z","iopub.execute_input":"2026-01-13T08:39:20.575871Z","iopub.status.idle":"2026-01-13T09:00:37.529905Z","shell.execute_reply.started":"2026-01-13T08:39:20.575854Z","shell.execute_reply":"2026-01-13T09:00:37.528974Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_DIR = os.getcwd()\n#CSV_PATH = os.path.join(BASE_DIR, 'train.csv')\nCSV_PATH = '/kaggle/input/csiro-biomass/train.csv'\nIMAGE_DIR = '/kaggle/input/csiro-biomass'\n#TEST_DIR = os.path.join(BASE_DIR, 'test')\nTEST_DIR ='/kaggle/input/csiro-biomass/test'\nMODEL_PATH = os.path.join(BASE_DIR, 'biomass_model.pth')\nBATCH_SIZE = 16\nTARGET_COLS = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n\n# Weights for the global R2 calculation\nWEIGHTS_MAP = {\n    'Dry_Green_g': 0.1,\n    'Dry_Dead_g': 0.1,\n    'Dry_Clover_g': 0.1,\n    'GDM_g': 0.2,\n    'Dry_Total_g': 0.5\n}\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {DEVICE}\")\nprint(f\"Base Directory: {BASE_DIR}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T09:02:49.691593Z","iopub.execute_input":"2026-01-13T09:02:49.691834Z","iopub.status.idle":"2026-01-13T09:02:49.698519Z","shell.execute_reply.started":"2026-01-13T09:02:49.691818Z","shell.execute_reply":"2026-01-13T09:02:49.697198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Data Preparation ---\ndef load_and_process_data(csv_path):\n    print(f\"Loading data from {csv_path}...\")\n    df = pd.read_csv(csv_path)\n    \n    df['image_id'] = df['image_path'].apply(lambda x: os.path.basename(x).split('.')[0])\n    \n    pivot_df = df.pivot_table(index='image_id', columns='target_name', values='target').reset_index()\n    pivot_df.fillna(0.0, inplace=True)\n    \n    meta_cols = ['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm']\n    meta_df = df.groupby('image_id')[meta_cols].first().reset_index()\n    \n    data = pd.merge(meta_df, pivot_df, on='image_id')\n    \n    data['Sampling_Date'] = pd.to_datetime(data['Sampling_Date'])\n    data['Month'] = data['Sampling_Date'].dt.month\n    \n    data['Pre_GSHH_NDVI'] = data['Pre_GSHH_NDVI'].fillna(data['Pre_GSHH_NDVI'].mean())\n    data['Height_Ave_cm'] = data['Height_Ave_cm'].fillna(data['Height_Ave_cm'].mean())\n    \n    return data\n\ndef prepare_test_dataframe(test_dir, train_df):\n    \"\"\"Scans test directory and creates a dataframe with default metadata from train.\"\"\"\n    if not os.path.exists(test_dir):\n        print(f\"Test directory {test_dir} not found.\")\n        return None\n    \n    image_files = [f for f in os.listdir(test_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]\n    if not image_files:\n        print(\"No images found in test directory.\")\n        return None\n        \n    print(f\"Found {len(image_files)} images in {test_dir}. Preparing metadata...\")\n    \n    # Calculate defaults from training data to fill missing metadata for test images\n    defaults = {\n        'Pre_GSHH_NDVI': train_df['Pre_GSHH_NDVI'].mean(),\n        'Height_Ave_cm': train_df['Height_Ave_cm'].mean(),\n        'Month': int(train_df['Month'].mode()[0]),\n        'State': train_df['State'].mode()[0],\n        'Species': train_df['Species'].mode()[0]\n    }\n    \n    data = []\n    for img_file in image_files:\n        img_id = os.path.splitext(img_file)[0]\n        # Image path relative to BASE_DIR (Dataset root_dir)\n        row = {'image_id': img_id, 'image_path': os.path.join('test', img_file), **defaults}\n        data.append(row)\n        \n    return pd.DataFrame(data)\n\ndef weighted_r2_score(y_true, y_pred, weights):\n    \"\"\"Computes the globally weighted R^2 score over all flattened pairs.\"\"\"\n    ss_res = np.sum(weights * (y_true - y_pred) ** 2)\n    weighted_mean_true = np.sum(weights * y_true) / np.sum(weights)\n    ss_tot = np.sum(weights * (y_true - weighted_mean_true) ** 2)\n    \n    if ss_tot == 0:\n        return 0.0\n    return 1 - (ss_res / ss_tot)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T09:02:59.660790Z","iopub.execute_input":"2026-01-13T09:02:59.661073Z","iopub.status.idle":"2026-01-13T09:02:59.670320Z","shell.execute_reply.started":"2026-01-13T09:02:59.661053Z","shell.execute_reply":"2026-01-13T09:02:59.669643Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Dataset Class ---\nclass BiomassDataset(Dataset):\n    def __init__(self, dataframe, root_dir, transform=None, meta_scaler=None, one_hot_encoder=None, is_train=True):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.root_dir = root_dir\n        self.transform = transform\n        self.is_train = is_train\n        \n        self.num_features = dataframe[['Pre_GSHH_NDVI', 'Height_Ave_cm', 'Month']].values.astype(np.float32)\n        cat_features = dataframe[['State', 'Species']].values\n        \n        if is_train:\n            self.ohe = OneHotEncoder(handle_unknown='ignore', sparse_output=False)\n            self.ohe_features = self.ohe.fit_transform(cat_features)\n            \n            self.scaler = StandardScaler()\n            self.features = np.hstack([self.num_features, self.ohe_features])\n            self.features = self.scaler.fit_transform(self.features)\n        else:\n            self.ohe = one_hot_encoder\n            self.ohe_features = self.ohe.transform(cat_features)\n            \n            self.scaler = meta_scaler\n            self.features = np.hstack([self.num_features, self.ohe_features])\n            self.features = self.scaler.transform(self.features)\n            \n        if all(col in dataframe.columns for col in TARGET_COLS):\n            self.targets = dataframe[TARGET_COLS].values.astype(np.float32)\n        else:\n            self.targets = np.zeros((len(dataframe), len(TARGET_COLS)), dtype=np.float32)\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, idx):\n        img_name = self.dataframe.iloc[idx]['image_path']\n        img_path = os.path.join(self.root_dir, img_name)\n        try:\n            image = Image.open(img_path).convert('RGB')\n        except (FileNotFoundError, OSError):\n            if idx == 0: # Print warning once to avoid spamming, but alert user\n                print(f\"Warning: Image not found at {img_path}. Using black image fallback. Check your IMAGE_DIR.\")\n            image = Image.new('RGB', (224, 224))\n\n        if self.transform:\n            image = self.transform(image)\n            \n        features = torch.tensor(self.features[idx], dtype=torch.float32)\n        target = torch.tensor(self.targets[idx], dtype=torch.float32)\n        return image, features, target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T09:03:12.476899Z","iopub.execute_input":"2026-01-13T09:03:12.477203Z","iopub.status.idle":"2026-01-13T09:03:12.486258Z","shell.execute_reply.started":"2026-01-13T09:03:12.477183Z","shell.execute_reply":"2026-01-13T09:03:12.485500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Model Architecture ---\nclass BiomassPredictor(nn.Module):\n    def __init__(self, num_meta_features, num_targets):\n        super(BiomassPredictor, self).__init__()\n        \n        # 1. Initialize empty ResNet18\n        self.cnn = models.resnet18(weights=None) \n        \n        # 2. LOAD LOCAL WEIGHTS HERE\n        WEIGHTS_PATH = '/kaggle/input/pytorch-biomass/resnet18.pth'\n        try:\n            # Use weights_only=False to fix the error you saw earlier\n            state_dict = torch.load(WEIGHTS_PATH, map_location='cpu', weights_only=False)\n            self.cnn.load_state_dict(state_dict)\n            print(\"Pre-trained ResNet18 weights loaded successfully!\")\n        except Exception as e:\n            print(f\"Error loading weights: {e}\")\n            print(\"Model is using RANDOM initialization.\")\n\n        # 3. Modify layers\n        self.cnn.fc = nn.Identity()\n        \n        self.meta_fc = nn.Sequential(\n            nn.Linear(num_meta_features, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, 32),\n            nn.ReLU()\n        )\n        \n        self.final_fc = nn.Sequential(\n            # 512 (from ResNet) + 32 (from meta_fc)\n            nn.Linear(512 + 32, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_targets)\n        )\n\n    def forward(self, image, meta_features):\n        x1 = self.cnn(image)\n        x2 = self.meta_fc(meta_features)\n        \n        # Concatenate image features and tabular data\n        x = torch.cat((x1, x2), dim=1)\n        return self.final_fc(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T09:05:51.565448Z","iopub.execute_input":"2026-01-13T09:05:51.565716Z","iopub.status.idle":"2026-01-13T09:05:51.572409Z","shell.execute_reply.started":"2026-01-13T09:05:51.565697Z","shell.execute_reply":"2026-01-13T09:05:51.571691Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Main Execution ---\n\n# 1. Load and Split Data\nfull_df = load_and_process_data(CSV_PATH)\ntrain_df, val_df = train_test_split(full_df, test_size=0.2, random_state=42)\n\n# 2. Setup Datasets (Fit scalers on train, apply to val)\nval_transforms = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])\n])\n\n# Dummy transform for train just to initialize the dataset class and fit scalers\ntrain_dataset = BiomassDataset(train_df, IMAGE_DIR, transform=val_transforms, is_train=True)\nval_dataset = BiomassDataset(val_df, IMAGE_DIR, transform=val_transforms, \n                             meta_scaler=train_dataset.scaler, \n                             one_hot_encoder=train_dataset.ohe, is_train=False)\n\nval_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)\n\n# 3. Load Model\nnum_meta_features = train_dataset.features.shape[1]\nmodel = BiomassPredictor(num_meta_features, num_targets=len(TARGET_COLS)).to(DEVICE)\n\nif os.path.exists(MODEL_PATH):\n    model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))\n    print(f\"Loaded model from {MODEL_PATH}\")\nelse:\n    raise FileNotFoundError(f\"Model not found at {MODEL_PATH}\")\n\n# 4. Predict\nmodel.eval()\nall_preds, all_targets = [], []\nwith torch.no_grad():\n    for images, meta, targets in val_loader:\n        images, meta = images.to(DEVICE), meta.to(DEVICE)\n        outputs = model(images, meta)\n        all_preds.append(outputs.cpu().numpy())\n        all_targets.append(targets.numpy())\n        \n# 5. Calculate Weighted R2\ny_pred = np.vstack(all_preds).flatten()\ny_true = np.vstack(all_targets).flatten()\n\n# Create weights vector matching the flattened data (repeating weights for each sample)\n# TARGET_COLS order determines the order in the flattened vector\nweights_per_row = [WEIGHTS_MAP[col] for col in TARGET_COLS]\nweights_flat = np.tile(weights_per_row, (len(val_dataset), 1)).flatten()\n\nscore = weighted_r2_score(y_true, y_pred, weights_flat)\nprint(f\"\\nGlobally Weighted R² Score: {score:.5f}\")\n\n# 6. Generate Submission File (Inference on Test Folder)\nprint(\"\\n--- Starting Test Inference ---\")\ntest_df = prepare_test_dataframe(TEST_DIR, full_df)\n\nif test_df is not None:\n    test_dataset = BiomassDataset(test_df, IMAGE_DIR, transform=val_transforms,\n                                  meta_scaler=train_dataset.scaler,\n                                  one_hot_encoder=train_dataset.ohe, is_train=False)\n    test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)\n    \n    test_preds = []\n    with torch.no_grad():\n        for images, meta, _ in test_loader:\n            images, meta = images.to(DEVICE), meta.to(DEVICE)\n            outputs = model(images, meta)\n            test_preds.append(outputs.cpu().numpy())\n    \n    predictions = np.vstack(test_preds)\n    image_ids = test_df['image_id'].tolist()\nelse:\n    print(\"Skipping test inference (no data found). Using validation predictions for demo.\")\n    predictions = np.vstack(all_preds)\n    image_ids = val_dataset.dataframe['image_id'].tolist()\n\n\nsubmission_file = os.path.join(BASE_DIR, f\"submission.csv\")\nprint(f\"Generating submission file: {submission_file}...\")\n\nsubmission_rows = []\nfor idx, img_id in enumerate(image_ids):\n    preds = predictions[idx]\n    for i, target_name in enumerate(TARGET_COLS):\n        sample_id = f\"{img_id}__{target_name}\"\n        submission_rows.append({'sample_id': sample_id, 'target': preds[i]})\n        \nsubmission_df = pd.DataFrame(submission_rows)\nsubmission_df.to_csv(submission_file, index=False)\nprint(f\"Submission saved to {submission_file}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-13T09:06:10.550765Z","iopub.execute_input":"2026-01-13T09:06:10.551065Z","iopub.status.idle":"2026-01-13T09:06:16.223944Z","shell.execute_reply.started":"2026-01-13T09:06:10.551031Z","shell.execute_reply":"2026-01-13T09:06:16.222350Z"}},"outputs":[],"execution_count":null}]}