{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":678155,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":514310,"modelId":528952}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"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\n\"\"\"\nDeep Learning Model for Regression\nConverts notebook cells into a standalone Python script\n\"\"\"\n\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\nimport torch\nimport random\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torchvision import models\nfrom torchvision.transforms import RandomErasing\n\nfrom collections import Counter\nimport numpy as np\nimport cv2\nimport os\nimport pandas as pd\nfrom torch.cuda.amp import autocast, GradScaler\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import r2_score, mean_absolute_error\nfrom sklearn.model_selection import train_test_split\nimport timm\n\n# ============================================================================\n# Seed Setup\n# ============================================================================\n\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(42)\n\n\n# ============================================================================\n# GPU Check\n# ============================================================================\n\nif torch.cuda.is_available():\n    print(\"GPU Name:\", torch.cuda.get_device_name(torch.cuda.current_device()))\nelse:\n    print(\"No CUDA device available\")\n\n\n# ============================================================================\n# Configuration and Transforms\n# ============================================================================\n\ndata_dir =\"/kaggle/input/csiro-biomass\"\n# Your mean/std\nmean = [0.485, 0.456, 0.406]\nstd = [0.229, 0.224, 0.225]\n\n# Define transform\ntrain_transform = A.Compose([\n    A.Resize(224, 224),\n    A.HorizontalFlip(p=0.5),\n    A.VerticalFlip(p=0.5),\n    A.RandomBrightnessContrast(p=0.2),\n    A.HueSaturationValue(p=0.2),\n    A.Rotate(limit=30, p=0.5),\n    A.ShiftScaleRotate(\n        shift_limit=0.1,\n        scale_limit=0.1,\n        rotate_limit=30,\n        p=0.5\n    ),\n    A.GridDistortion(p=0.5),\n    A.GaussianBlur(blur_limit=(3, 7), p=0.5),\n    A.CLAHE(p=0.5),\n    A.RGBShift(p=0.5),\n    A.Normalize(mean=mean, std=std),\n    ToTensorV2(),\n])\n\ntest_transform = A.Compose([\n    A.Resize(224, 224),\n    A.Normalize(mean=mean, std=std),\n    ToTensorV2(),\n])\n\n\n# ============================================================================\n# Dataset Class for train and validation\n# ============================================================================\n\nclass GroupedDataset(Dataset):\n    def __init__(self, dataframe, transform=None, base_target_names=None):\n        \"\"\"\n        Args:\n            dataframe: Pandas DataFrame with columns ['image_path', 'target', 'target_name']\n            transform: Albumentations or other transform\n            base_target_names: list of 3 target names that are the base variables\n        \"\"\"\n        self.groups = list(dataframe.groupby(\"image_path\"))\n        self.transform = transform\n        self.base_target_names = base_target_names\n\n    def __len__(self):\n        return len(self.groups)\n\n    def __getitem__(self, idx):\n        image_path_loc, group = self.groups[idx]\n        image_path = os.path.join(data_dir, image_path_loc)\n\n        # Load image with PIL\n        img = Image.open(image_path).convert(\"RGB\")\n        img = np.array(img)  # Albumentations expects numpy arrays\n\n        if self.transform:\n            img = self.transform(image=img)[\"image\"]\n\n        # Extract targets and names\n        targets = group[\"target\"].values.astype(\"float32\")\n        target_names = group[\"target_name\"].values.tolist()\n\n        # --- Select only the base targets (the 3 you predict) ---\n        mask = [name in self.base_target_names for name in target_names]\n        base_targets = targets[mask]\n        base_names = [name for name in target_names if name in self.base_target_names]\n\n        img_id = image_path_loc.split(\"train/\")[1]\n        return {\n            \"image\": img,\n            \"base_targets\": torch.tensor(base_targets),\n            \"base_names\": base_names,\n            \"img_id\": img_id\n        }\n\n\n# ============================================================================\n# Data Loading\n# ============================================================================\ndef generate_train_test_ids(df_path):\n    df=pd.read_csv(df_path)\n    # unique images\n    unique_images = df.drop_duplicates(subset=['image_path'])\n\n\n    # Stratify by both State and Species\n    unique_images['stratify_col'] = unique_images['State'].astype(str) + '_' + unique_images['Species'].astype(str)\n\n    train_idx, val_idx = train_test_split(\n        unique_images.index,\n        test_size=0.2,\n        stratify=unique_images['stratify_col'],\n        random_state=42\n    )\n\n    validation_set = unique_images.loc[val_idx]\n\n    validation_full = df[df['image_path'].isin(validation_set['image_path'])]\n\n    #print(validation_full.head(10))\n    print(f\"Validation set with all columns shape: {validation_full.shape}\")\n\n    # # Optionally, save to CSV\n    validation_full.to_csv('validation_list.csv', index=False)\n\n    # Create train_df by removing validation_full from df\n    train_df = df[~df['image_path'].isin(validation_full['image_path'])]\n\n    #print(train_df.head(10))\n    print(f\"Train set shape: {train_df.shape}\")\n    # Optionally, save to CSV\n    train_df.to_csv('train_list.csv', index=False)\n\n    \ndef load_data(batch_size=8):\n    \"\"\"Load training and validation data\"\"\"\n    train_df = pd.read_csv(\"train_list.csv\")\n    train_dataset = GroupedDataset(\n        train_df,\n        transform=train_transform,\n        base_target_names=[\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\"]\n    )\n    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)\n\n    valid_df = pd.read_csv(\"validation_list.csv\")\n    valid_dataset = GroupedDataset(\n        valid_df,\n        transform=test_transform,\n        base_target_names=[\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\"]\n    )\n    valid_loader = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False)\n\n    return train_loader, valid_loader\n\n\n\nclass GroupedTestDataset(Dataset):\n    def __init__(self, dataframe, transform=None, data_dir=None):\n        \"\"\"\n        Args:\n            dataframe: Pandas DataFrame with at least ['image_path'] column\n            transform: Albumentations or other transform\n            data_dir: base directory where images are stored\n        \"\"\"\n        # Group by image_path to ensure uniqueness\n        self.groups = list(dataframe.groupby(\"image_path\"))\n        self.transform = transform\n        self.data_dir = data_dir\n\n    def __len__(self):\n        return len(self.groups)\n\n    def __getitem__(self, idx):\n        image_path_loc, group = self.groups[idx]\n        image_path = os.path.join(self.data_dir, image_path_loc)\n\n        # Load image\n        img = Image.open(image_path).convert(\"RGB\")\n        img = np.array(img)\n\n        if self.transform:\n            img = self.transform(image=img)[\"image\"]\n\n        # Since test.csv has no targets, just return image + ID\n        img_id = os.path.basename(image_path_loc)\n\n        return {\n            \"image\": img,\n            \"img_id\": img_id\n        }\ndef test_loader(batch_size=8):\n    \"\"\"Load test data\"\"\"\n    test_df = pd.read_csv(os.path.join(data_dir,\"test.csv\"))\n    test_dataset = GroupedTestDataset(test_df, transform=test_transform, data_dir=data_dir)\n\n\n    test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)\n\n    return test_loader\n# ============================================================================\n# Model Architecture\n# ============================================================================\n\nclass ResNetRegression(nn.Module):\n    def __init__(self, target_names, dropout=0.3, freeze_backbone=False):\n        super().__init__()\n\n       \n\n        # --- Load Torchvision ResNet50 ---\n        self.backbone = models.resnet50(weights=None)   # no internet\n        \n        # Load local .pth file\n        state_dict = torch.load(\"/kaggle/input/resnet50/pytorch/default/1/resnet50-11ad3fa6.pth\")\n        self.backbone.load_state_dict(state_dict)\n\n\n        # Optionally freeze feature extractor\n        if freeze_backbone:\n            for param in self.backbone.parameters():\n                param.requires_grad = False\n\n        # Number of features from ResNet's last layer\n        in_features = self.backbone.fc.in_features\n\n        # Replace FC with a regression head\n        self.backbone.fc = nn.Sequential(\n            nn.Linear(in_features, 512),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(512, len(target_names))   # output size = number of targets\n        )\n\n        # Save target names\n        self.target_names = target_names\n\n    def forward(self, x):\n        return self.backbone(x)\n\n\n# ============================================================================\n# Evaluation Function\n# ============================================================================\n\ndef evaluate_model(model, dataloader, device):\n    \"\"\"Evaluate model on validation/test data\"\"\"\n    model.eval()\n    all_preds = []\n    all_targets = []\n\n    with torch.no_grad():\n        for batch in dataloader:\n            imgs = batch[\"image\"].to(device)\n            targets = batch[\"base_targets\"].cpu().numpy()  # shape (B, 3)\n\n            outputs = model(imgs).cpu().numpy()  # shape (B, 3)\n\n            # Base predictions\n            pred_clover = outputs[:, 0]\n            pred_dead = outputs[:, 1]\n            pred_green = outputs[:, 2]\n\n            # Derived predictions\n            pred_gdm = pred_green + pred_clover\n            pred_total = pred_gdm + pred_dead\n\n            # Base targets\n            tgt_clover = targets[:, 0]\n            tgt_dead = targets[:, 1]\n            tgt_green = targets[:, 2]\n\n            # Derived targets\n            tgt_gdm = tgt_green + tgt_clover\n            tgt_total = tgt_gdm + tgt_dead\n\n            # Stack predictions and targets (B, 5)\n            preds = np.stack([pred_clover, pred_dead, pred_green, pred_gdm, pred_total], axis=1)\n            tgts = np.stack([tgt_clover, tgt_dead, tgt_green, tgt_gdm, tgt_total], axis=1)\n\n            all_preds.append(preds)\n            all_targets.append(tgts)\n\n    # Concatenate across batches\n    all_preds = np.concatenate(all_preds, axis=0)\n    all_targets = np.concatenate(all_targets, axis=0)\n\n    # Define weights per target type\n    weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])  # order: Clover, Dead, Green, GDM, Total\n    target_names = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n    # --- Weighted R² calculation ---\n    ss_res = np.sum(weights * np.sum((all_targets - all_preds) ** 2, axis=0))\n    ss_tot = np.sum(weights * np.sum((all_targets - np.mean(all_targets, axis=0)) ** 2, axis=0))\n    weighted_r2 = 1 - ss_res / ss_tot\n\n    # --- Per-target R² values ---\n    per_target_r2 = {}\n    for i, name in enumerate(target_names):\n        per_target_r2[name] = r2_score(all_targets[:, i], all_preds[:, i])\n\n    return weighted_r2, per_target_r2\n\n\n# ============================================================================\n# Training Function\n# ============================================================================\n\ndef train_model(model, train_loader, valid_loader, device, num_epochs=200, patience=35, checkpoint_dir=\"checkpoints\"):\n    \"\"\"Train the model\"\"\"\n    os.makedirs(checkpoint_dir, exist_ok=True)\n\n    optimizer = optim.Adam(model.parameters(), lr=1e-4)\n    criterion = nn.MSELoss()\n\n    train_losses = []\n    val_losses = []\n    train_mae_scores = []\n    val_mae_scores = []\n    val_r2_scores = []\n\n    best_val_r2 = -float('inf')\n    best_epoch = 0\n\n    for epoch in range(num_epochs):\n        model.train()\n        running_loss = 0.0\n        all_train_targets = []\n        all_train_preds = []\n\n        # --- Training loop ---\n        for batch in train_loader:\n            imgs = batch[\"image\"].to(device)  # shape (B, 3, H, W)\n            targets = batch[\"base_targets\"].to(device)  # shape (B, 3)\n\n            optimizer.zero_grad()\n            outputs = model(imgs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n\n            running_loss += loss.item() * imgs.size(0)\n            all_train_targets.append(targets.cpu().numpy())\n            all_train_preds.append(outputs.detach().cpu().numpy())\n\n        epoch_train_loss = running_loss / len(train_loader.dataset)\n        train_losses.append(epoch_train_loss)\n\n        # Compute train MAE\n        all_train_targets = np.concatenate(all_train_targets, axis=0)\n        all_train_preds = np.concatenate(all_train_preds, axis=0)\n        train_mae = mean_absolute_error(all_train_targets, all_train_preds)\n        train_mae_scores.append(train_mae)\n\n        # --- Validation loop ---\n        model.eval()\n        val_loss = 0.0\n        all_targets = []\n        all_preds = []\n        with torch.no_grad():\n            for batch in valid_loader:\n                imgs = batch[\"image\"].to(device)\n                targets = batch[\"base_targets\"].to(device)\n\n                outputs = model(imgs)\n                loss = criterion(outputs, targets)\n                val_loss += loss.item() * imgs.size(0)\n\n                all_targets.append(targets.cpu().numpy())\n                all_preds.append(outputs.cpu().numpy())\n\n        epoch_val_loss = val_loss / len(valid_loader.dataset)\n        val_losses.append(epoch_val_loss)\n\n        # Validation MAE\n        all_targets = np.concatenate(all_targets, axis=0)\n        all_preds = np.concatenate(all_preds, axis=0)\n        val_mae = mean_absolute_error(all_targets, all_preds)\n        val_mae_scores.append(val_mae)\n\n        # --- Weighted R² evaluation ---\n        weighted_r2, per_target_r2 = evaluate_model(model, valid_loader, device)\n        val_r2_scores.append(weighted_r2)\n\n        print(f\"Epoch {epoch+1}: Train Loss={epoch_train_loss:.4f}, Train MAE={train_mae:.4f}, \"\n              f\"Val Loss={epoch_val_loss:.4f}, Val MAE={val_mae:.4f}, Val Weighted R²={weighted_r2:.4f}\")\n\n        # Early stopping and checkpoint based on weighted R²\n        if weighted_r2 > best_val_r2:\n            best_val_r2 = weighted_r2\n            best_epoch = epoch\n            torch.save(model.state_dict(), os.path.join(checkpoint_dir, \"best_model.pt\"))\n            print(\"Best model saved.\")\n        elif epoch - best_epoch >= patience:\n            print(\"Early stopping triggered.\")\n            break\n\n    # Save final logs\n    np.save(os.path.join(checkpoint_dir, \"train_losses.npy\"), np.array(train_losses))\n    np.save(os.path.join(checkpoint_dir, \"val_losses.npy\"), np.array(val_losses))\n    np.save(os.path.join(checkpoint_dir, \"train_mae_scores.npy\"), np.array(train_mae_scores))\n    np.save(os.path.join(checkpoint_dir, \"val_mae_scores.npy\"), np.array(val_mae_scores))\n    np.save(os.path.join(checkpoint_dir, \"val_r2_scores.npy\"), np.array(val_r2_scores))\n\n    # Write metrics to a text file\n    with open(os.path.join(checkpoint_dir, \"training_log.txt\"), \"w\") as f:\n        for epoch, (t_loss, v_loss, t_mae, v_mae, v_r2) in enumerate(\n            zip(train_losses, val_losses, train_mae_scores, val_mae_scores, val_r2_scores), 1\n        ):\n            f.write(f\"Epoch {epoch}: Train Loss={t_loss:.4f}, Train MAE={t_mae:.4f}, \"\n                    f\"Val Loss={v_loss:.4f}, Val MAE={v_mae:.4f}, Val Weighted R²={v_r2:.4f}\\n\")\n        f.write(f\"\\nBest Epoch: {best_epoch+1}, Best Val Weighted R²={best_val_r2:.4f}\\n\")\n\n    return train_losses, val_losses, train_mae_scores, val_mae_scores, val_r2_scores\n\n\n\n\n\ndef evaluate_test_data(model, dataloader, device, output_csv=\"submission.csv\"):\n    \"\"\"Evaluate model on validation/test data and save predictions to CSV\"\"\"\n    model.eval()\n    all_preds = []\n    all_img_ids = []\n\n    with torch.no_grad():\n        for batch in dataloader:\n            imgs = batch[\"image\"].to(device)\n            img_ids = batch[\"img_id\"]   # list of image ids\n\n            outputs = model(imgs).cpu().numpy()  # shape (B, 3)\n\n            # Base predictions\n            pred_clover = outputs[:, 0]\n            pred_dead = outputs[:, 1]\n            pred_green = outputs[:, 2]\n\n            # Derived predictions\n            pred_gdm = pred_green + pred_clover\n            pred_total = pred_gdm + pred_dead\n\n            # Stack predictions (B, 5)\n            preds = np.stack([pred_clover, pred_dead, pred_green, pred_gdm, pred_total], axis=1)\n            all_preds.append(preds)\n            all_img_ids.extend(img_ids)\n\n    # Concatenate across batches\n    all_preds = np.concatenate(all_preds, axis=0)\n\n    target_names = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\",  \"GDM_g\",\"Dry_Total_g\"]\n\n    # Build rows for CSV\n    rows = []\n    for img_id, pred_row in zip(all_img_ids, all_preds):\n        # remove extension if present\n        base_id = os.path.splitext(img_id)[0]\n        for tname, pval in zip(target_names, pred_row):\n            sample_id = f\"{base_id}__{tname}\"\n            rows.append({\"sample_id\": sample_id, \"target\": pval})\n\n    df = pd.DataFrame(rows)\n    df.to_csv(output_csv, index=False)\n\n    return df\n# ============================================================================\n# Main Execution\n# ============================================================================\n\nif __name__ == \"__main__\":\n    # Set device\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(f\"Using device: {device}\")\n    \n    train_df_path=\"/kaggle/input/csiro-biomass/train.csv\"\n    generate_train_test_ids(train_df_path)\n    #Load data\n    train_loader, valid_loader = load_data(batch_size=8)\n  \n    # \n    # # Create model\n    target_names = [\"Dry_Clover_g\", \"Dry_Dead_g\", \"Dry_Green_g\"]\n    model = ResNetRegression(target_names=target_names, freeze_backbone=False)\n    # print(f\"Model target names: {model.target_names}\")\n\n    # Move model to device\n    model = model.to(device)\n\n    # # Train model\n    train_losses, val_losses, train_mae_scores, val_mae_scores, val_r2_scores = train_model(\n        model,\n        train_loader,\n        valid_loader,\n        device,\n        num_epochs=200,\n        patience=35,\n        checkpoint_dir=\"checkpoints\"\n    )\n\n    print(\"Training completed!\")\n\n    test_loader= test_loader(batch_size=5)\n    checkpoint_path = \"/kaggle/working/checkpoints/best_model.pt\"   # adjust to your actual file\n    checkpoint = torch.load(checkpoint_path, map_location=device)\n\n    # 3. Restore weights\n    model.load_state_dict(checkpoint)   # or checkpoint if you saved only state_dict\n\n    # 4. Move to device\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    model.to(device)\n\n\n\n    evaluate_test_data(model,test_loader,device, output_csv=\"submission.csv\")\n    print('submission.csv saved')\n\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-09T18:48:44.380085Z","iopub.execute_input":"2025-12-09T18:48:44.380820Z","iopub.status.idle":"2025-12-09T19:34:31.804871Z","shell.execute_reply.started":"2025-12-09T18:48:44.380794Z","shell.execute_reply":"2025-12-09T19:34:31.803786Z"}},"outputs":[],"execution_count":null}]}