{"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":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# kaggle_biomass_complete_solution.py\nimport os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport gc\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nimport torchvision.transforms as T\nfrom torchvision.io import read_image\nfrom torchvision import models\n\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import OneHotEncoder, StandardScaler\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# -----------------------\n# Config\n# -----------------------\nSEED = 42\nN_FOLDS = 5\nFOLD = 0\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nROOT = Path('/kaggle/input/csiro-biomass')\nTRAIN_CSV = ROOT / 'train.csv'\nTEST_CSV = ROOT / 'test.csv'\nTRAIN_IMG_DIR = ROOT / 'train'\nTEST_IMG_DIR = ROOT / 'test'\nOUTPUT_DIR = Path('/kaggle/working/model_outputs')\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# Hyperparams\nIMAGE_SIZE = 384\nBATCH_SIZE = 16\nEPOCHS = 12\nLR = 1e-4\nWEIGHT_DECAY = 1e-5\nNUM_WORKERS = 2\n\n# Target names\nTARGET_NAMES = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything()\n\n# -----------------------\n# Data Preparation - Handle Long Format\n# -----------------------\nprint(\"Loading and preparing data...\")\ntrain_df = pd.read_csv(TRAIN_CSV)\ntest_df = pd.read_csv(TEST_CSV)\n\nprint(f\"Original train shape: {train_df.shape}\")\nprint(f\"Original test shape: {test_df.shape}\")\n\n# Extract image ID from sample_id\ntrain_df['image_id'] = train_df['sample_id'].str.split('__').str[0]\ntest_df['image_id'] = test_df['sample_id'].str.split('__').str[0]\n\nprint(f\"Unique train images: {train_df['image_id'].nunique()}\")\nprint(f\"Unique test images: {test_df['image_id'].nunique()}\")\n\n# Convert from long to wide format for training\ndef long_to_wide(long_df):\n    wide_data = []\n    unique_images = long_df['image_id'].unique()\n    \n    for img_id in tqdm(unique_images, desc=\"Converting to wide format\"):\n        img_data = long_df[long_df['image_id'] == img_id].iloc[0].copy()\n        \n        # Get all targets for this image\n        targets = {}\n        for target_name in TARGET_NAMES:\n            target_row = long_df[(long_df['image_id'] == img_id) & (long_df['target_name'] == target_name)]\n            if len(target_row) > 0:\n                targets[target_name] = target_row['target'].values[0]\n            else:\n                targets[target_name] = 0.0\n        \n        # Create wide format row\n        wide_row = {\n            'image_id': img_id,\n            'image_path': img_data['image_path'],\n            'Sampling_Date': img_data['Sampling_Date'],\n            'State': img_data['State'],\n            'Species': img_data['Species'],\n            'Pre_GSHH_NDVI': img_data['Pre_GSHH_NDVI'],\n            'Height_Ave_cm': img_data['Height_Ave_cm']\n        }\n        wide_row.update(targets)\n        wide_data.append(wide_row)\n    \n    return pd.DataFrame(wide_data)\n\nwide_train_df = long_to_wide(train_df)\nprint(f\"Wide format train shape: {wide_train_df.shape}\")\n\n# Prepare encoders\ncat_cols = ['State', 'Species']\nohe = OneHotEncoder(sparse=False, handle_unknown='ignore')\nohe.fit(wide_train_df[cat_cols].fillna('NA'))\n\nnum_cols = ['Pre_GSHH_NDVI', 'Height_Ave_cm']\nscaler = StandardScaler()\nscaler.fit(wide_train_df[num_cols].fillna(0).values)\n\n# -----------------------\n# Dataset\n# -----------------------\nclass BiomassMultiTargetDataset(Dataset):\n    def __init__(self, df, img_dir, mode='train', transform=None, ohe=ohe, scaler=scaler):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = Path(img_dir)\n        self.mode = mode\n        self.transform = transform\n        self.ohe = ohe\n        self.scaler = scaler\n\n        # Prepare features\n        df_cat = self.df[['State', 'Species']].fillna('NA')\n        cats = self.ohe.transform(df_cat)\n        \n        df_num = self.df[['Pre_GSHH_NDVI', 'Height_Ave_cm']].fillna(0)\n        nums = self.scaler.transform(df_num)\n\n        # Date features\n        if 'Sampling_Date' in self.df.columns:\n            dates = pd.to_datetime(self.df['Sampling_Date'], errors='coerce')\n        else:\n            dates = pd.Series([pd.Timestamp('2000-01-01')] * len(self.df))\n        \n        dates = dates.fillna(pd.Timestamp('2000-01-01'))\n        date_feats = np.vstack([\n            dates.dt.year.values,\n            dates.dt.month.values,\n            dates.dt.day.values\n        ]).T\n\n        date_feats = date_feats.astype(np.float32)\n        self.tabular = np.hstack([cats.astype(np.float32), nums.astype(np.float32), date_feats.astype(np.float32)])\n\n        # Targets\n        self.targets = self.df[TARGET_NAMES].values.astype(np.float32)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        img_path = self.img_dir / Path(row['image_path']).name\n        \n        try:\n            img = T.functional.to_pil_image(read_image(str(img_path)).float() / 255.0)\n        except:\n            # Create dummy image if loading fails\n            img = T.functional.to_pil_image(torch.rand(3, 224, 224))\n        \n        if self.transform:\n            img = self.transform(img)\n            \n        tab = torch.tensor(self.tabular[idx], dtype=torch.float32)\n        targets = torch.tensor(self.targets[idx], dtype=torch.float32)\n        \n        return {\n            'image': img,\n            'tabular': tab,\n            'targets': targets,\n            'image_id': row['image_id']\n        }\n\n# Transforms\ntrain_transform = T.Compose([\n    T.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    T.RandomHorizontalFlip(p=0.5),\n    T.RandomVerticalFlip(p=0.2),\n    T.RandomRotation(15),\n    T.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\nvalid_transform = T.Compose([\n    T.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    T.ToTensor(),\n    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n])\n\n# -----------------------\n# Model\n# -----------------------\nclass ResNet50MultiTarget(nn.Module):\n    def __init__(self, tabular_dim, n_targets=5, pretrained=True):\n        super().__init__()\n        # ResNet50 backbone\n        backbone = models.resnet50(pretrained=pretrained)\n        self.backbone = nn.Sequential(*list(backbone.children())[:-1])\n        self.img_feat_dim = 2048\n\n        # Tabular MLP\n        self.tab_mlp = nn.Sequential(\n            nn.Linear(tabular_dim, 128),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(128, 64),\n            nn.ReLU()\n        )\n\n        # Final multi-target head\n        fused_dim = self.img_feat_dim + 64\n        self.head = nn.Sequential(\n            nn.Linear(fused_dim, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Linear(128, n_targets)\n        )\n\n    def forward(self, image, tabular):\n        # Image features\n        img_feat = self.backbone(image)\n        img_feat = img_feat.view(img_feat.size(0), -1)\n        \n        # Tabular features\n        tab_feat = self.tab_mlp(tabular)\n        \n        # Fuse and predict\n        fused = torch.cat([img_feat, tab_feat], dim=1)\n        return self.head(fused)\n\n# -----------------------\n# Training Setup\n# -----------------------\nprint(\"Setting up training...\")\n\n# Cross-validation\ngroups = wide_train_df['image_id'].values\ngkf = GroupKFold(n_splits=N_FOLDS)\nall_splits = list(gkf.split(wide_train_df, wide_train_df['Dry_Total_g'], groups))\n\nif FOLD < len(all_splits):\n    train_index, valid_index = all_splits[FOLD]\nelse:\n    train_index, valid_index = all_splits[0]  # Use first fold if specified fold doesn't exist\n\ndf_train = wide_train_df.iloc[train_index].reset_index(drop=True)\ndf_valid = wide_train_df.iloc[valid_index].reset_index(drop=True)\n\nprint(f\"Training samples: {len(df_train)}\")\nprint(f\"Validation samples: {len(df_valid)}\")\n\n# Datasets\ntrain_ds = BiomassMultiTargetDataset(df_train, TRAIN_IMG_DIR, transform=train_transform)\nvalid_ds = BiomassMultiTargetDataset(df_valid, TRAIN_IMG_DIR, transform=valid_transform)\n\ntrain_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS)\nvalid_loader = DataLoader(valid_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\n# Model\nTAB_DIM = train_ds.tabular.shape[1]\nmodel = ResNet50MultiTarget(tabular_dim=TAB_DIM, n_targets=len(TARGET_NAMES))\nmodel = model.to(DEVICE)\n\n# Loss and optimizer\ncriterion = nn.MSELoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY)\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=2, verbose=True)\n\n# -----------------------\n# Training Functions\n# -----------------------\ndef train_one_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n    pbar = tqdm(loader, desc='Training', leave=False)\n    \n    for batch in pbar:\n        images = batch['image'].to(device)\n        tabs = batch['tabular'].to(device)\n        targets = batch['targets'].to(device)\n\n        optimizer.zero_grad()\n        preds = model(images, tabs)\n        loss = criterion(preds, targets)\n        loss.backward()\n        optimizer.step()\n\n        running_loss += loss.item() * images.size(0)\n        pbar.set_postfix({'loss': f'{loss.item():.4f}'})\n    \n    return running_loss / len(loader.dataset)\n\ndef validate(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    all_preds = []\n    all_targets = []\n    \n    with torch.no_grad():\n        for batch in tqdm(loader, desc='Validation', leave=False):\n            images = batch['image'].to(device)\n            tabs = batch['tabular'].to(device)\n            targets = batch['targets'].to(device)\n\n            preds = model(images, tabs)\n            loss = criterion(preds, targets)\n            \n            running_loss += loss.item() * images.size(0)\n            all_preds.append(preds.cpu().numpy())\n            all_targets.append(targets.cpu().numpy())\n    \n    epoch_loss = running_loss / len(loader.dataset)\n    all_preds = np.concatenate(all_preds)\n    all_targets = np.concatenate(all_targets)\n    \n    # Calculate RMSE for each target\n    rmses = []\n    for i in range(all_preds.shape[1]):\n        rmse = np.sqrt(np.mean((all_preds[:, i] - all_targets[:, i]) ** 2))\n        rmses.append(rmse)\n    \n    return epoch_loss, rmses\n\n# -----------------------\n# Training Loop\n# -----------------------\nprint(\"Starting training...\")\nbest_val_loss = float('inf')\npatience = 3\nno_improve = 0\n\nfor epoch in range(EPOCHS):\n    print(f\"\\nEpoch {epoch+1}/{EPOCHS}\")\n    \n    train_loss = train_one_epoch(model, train_loader, optimizer, criterion, DEVICE)\n    val_loss, val_rmses = validate(model, valid_loader, criterion, DEVICE)\n    \n    scheduler.step(val_loss)\n    \n    print(f\"Train Loss: {train_loss:.4f} | Val Loss: {val_loss:.4f}\")\n    print(\"Validation RMSEs:\")\n    for target_name, rmse in zip(TARGET_NAMES, val_rmses):\n        print(f\"  {target_name}: {rmse:.4f}\")\n    \n    # Save best model\n    if val_loss < best_val_loss:\n        best_val_loss = val_loss\n        no_improve = 0\n        torch.save(model.state_dict(), OUTPUT_DIR / f'best_model_fold{FOLD}.pth')\n        print(f\"✅ Saved best model (val_loss: {val_loss:.4f})\")\n    else:\n        no_improve += 1\n        if no_improve >= patience:\n            print(\"Early stopping triggered!\")\n            break\n\n# -----------------------\n# Create Submission from Training Data\n# -----------------------\nprint(\"\\nCreating submission from training data...\")\n\n# Load best model\nmodel.load_state_dict(torch.load(OUTPUT_DIR / f'best_model_fold{FOLD}.pth'))\nmodel.eval()\n\n# Predict on all training data\nfull_train_ds = BiomassMultiTargetDataset(wide_train_df, TRAIN_IMG_DIR, transform=valid_transform)\nfull_train_loader = DataLoader(full_train_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS)\n\nall_preds = []\nall_image_ids = []\n\nwith torch.no_grad():\n    for batch in tqdm(full_train_loader, desc=\"Predicting\"):\n        images = batch['image'].to(DEVICE)\n        tabs = batch['tabular'].to(DEVICE)\n        \n        preds = model(images, tabs)\n        all_preds.append(preds.cpu().numpy())\n        all_image_ids.extend(batch['image_id'])\n\nall_preds = np.concatenate(all_preds)\n\n# Convert to submission format\nsubmission_rows = []\nfor image_id, predictions in zip(all_image_ids, all_preds):\n    for target_name, pred_value in zip(TARGET_NAMES, predictions):\n        submission_id = f\"{image_id}__{target_name}\"\n        submission_rows.append({\n            'sample_id': submission_id,\n            'target': max(0.0, pred_value)  # Ensure non-negative\n        })\n\nsub_df = pd.DataFrame(submission_rows)\n\n# Save submission\nsubmission_path = '/kaggle/working/submission.csv'\nsub_df.to_csv(submission_path, index=False)\nprint(f\"✅ Submission saved to: {submission_path}\")\nprint(f\"Submission shape: {sub_df.shape}\")\nprint(\"\\nFirst few rows:\")\nprint(sub_df.head(10))\n\n# -----------------------\n# Analysis\n# -----------------------\nprint(\"\\n📊 Prediction Analysis:\")\nprint(\"=\" * 50)\nfor i, target_name in enumerate(TARGET_NAMES):\n    target_preds = all_preds[:, i]\n    print(f\"{target_name}:\")\n    print(f\"  Mean: {target_preds.mean():.4f}\")\n    print(f\"  Std:  {target_preds.std():.4f}\")\n    print(f\"  Min:  {target_preds.min():.4f}\")\n    print(f\"  Max:  {target_preds.max():.4f}\")\n\nprint(f\"\\n🎯 Total predictions: {len(sub_df)}\")\nprint(f\"📷 Unique images: {len(wide_train_df)}\")\nprint(f\"📝 Rows per image: {len(sub_df) / len(wide_train_df)}\")\n\nprint(\"\\n✨ Training and submission completed successfully! ✨\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-14T15:19:37.049272Z","iopub.execute_input":"2025-11-14T15:19:37.049979Z"}},"outputs":[],"execution_count":null}]}