{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"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\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":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:31.555245Z","iopub.execute_input":"2026-04-22T19:02:31.555567Z","iopub.status.idle":"2026-04-22T19:02:32.339340Z","shell.execute_reply.started":"2026-04-22T19:02:31.555514Z","shell.execute_reply":"2026-04-22T19:02:32.338614Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Visualization Imports ───────────────────────────────────────────────────\nimport matplotlib.pyplot as plt\nimport matplotlib.gridspec as gridspec\nimport seaborn as sns\nimport numpy as np\nimport pandas as pd\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# Research-paper style\nplt.rcParams.update({\n    'font.family': 'DejaVu Serif',\n    'font.size': 12,\n    'axes.titlesize': 13,\n    'axes.labelsize': 12,\n    'xtick.labelsize': 10,\n    'ytick.labelsize': 10,\n    'legend.fontsize': 10,\n    'figure.dpi': 150,\n    'savefig.dpi': 300,\n    'savefig.bbox': 'tight',\n    'axes.grid': True,\n    'grid.alpha': 0.3,\n    'axes.spines.top': False,\n    'axes.spines.right': False,\n})\nPALETTE = sns.color_palette(\"deep\")\nprint(\"Visualization libraries loaded ✓\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:32.340177Z","iopub.execute_input":"2026-04-22T19:02:32.340630Z","iopub.status.idle":"2026-04-22T19:02:33.140466Z","shell.execute_reply.started":"2026-04-22T19:02:32.340595Z","shell.execute_reply":"2026-04-22T19:02:33.139553Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/competitions/csiro-biomass\"\n\nTRAIN_CSV = f\"{DATA_DIR}/train.csv\"\nTEST_CSV = f\"{DATA_DIR}/test.csv\"\n\nTRAIN_DIR = f\"{DATA_DIR}/train\"\nTEST_DIR = f\"{DATA_DIR}/test\"\n\nOUTPUT_DIR = \"/kaggle/working/output\"\n\nimport os\nos.makedirs(OUTPUT_DIR, exist_ok=True)\n\nprint(\"Train CSV:\", TRAIN_CSV)\nprint(\"Train images:\", TRAIN_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:33.142697Z","iopub.execute_input":"2026-04-22T19:02:33.143142Z","iopub.status.idle":"2026-04-22T19:02:33.149149Z","shell.execute_reply.started":"2026-04-22T19:02:33.143117Z","shell.execute_reply":"2026-04-22T19:02:33.148294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndf = pd.read_csv(TRAIN_CSV)\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:33.150193Z","iopub.execute_input":"2026-04-22T19:02:33.150487Z","iopub.status.idle":"2026-04-22T19:02:33.194944Z","shell.execute_reply.started":"2026-04-22T19:02:33.150464Z","shell.execute_reply":"2026-04-22T19:02:33.194170Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_wide = df.pivot_table(\n    index=[\"image_path\", \"Sampling_Date\", \"State\", \"Species\", \"Pre_GSHH_NDVI\", \"Height_Ave_cm\"],\n    columns=\"target_name\",\n    values=\"target\"\n).reset_index()\n\ndf_wide.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:33.197640Z","iopub.execute_input":"2026-04-22T19:02:33.197941Z","iopub.status.idle":"2026-04-22T19:02:33.267424Z","shell.execute_reply.started":"2026-04-22T19:02:33.197894Z","shell.execute_reply":"2026-04-22T19:02:33.266553Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Exploratory Data Analysis (EDA)\n\nUnderstanding the distribution of biomass targets is essential before training. The following visualizations characterize the dataset used for model development.\n","metadata":{}},{"cell_type":"code","source":"# ─── Figure 1: Target Variable Distributions ─────────────────────────────────\ntarget_cols = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\ntarget_labels = [\"Dry Green (g)\", \"Dry Dead (g)\", \"Dry Clover (g)\", \"GDM (g)\", \"Dry Total (g)\"]\n\nfig, axes = plt.subplots(2, 3, figsize=(14, 8))\naxes = axes.flatten()\n\nfor i, (col, label) in enumerate(zip(target_cols, target_labels)):\n    ax = axes[i]\n    data = df_wide[col].dropna()\n    sns.histplot(data, kde=True, ax=ax, color=PALETTE[i], bins=30, alpha=0.7, edgecolor='white')\n    ax.axvline(data.mean(), color='red', linestyle='--', linewidth=1.5, label=f'Mean: {data.mean():.1f}')\n    ax.axvline(data.median(), color='orange', linestyle=':', linewidth=1.5, label=f'Median: {data.median():.1f}')\n    ax.set_title(label, fontweight='bold')\n    ax.set_xlabel('Biomass (g)')\n    ax.set_ylabel('Count')\n    ax.legend(fontsize=8)\n\naxes[-1].set_visible(False)  # hide 6th subplot\nfig.suptitle('Figure 1: Distribution of Biomass Target Variables', fontsize=14, fontweight='bold', y=1.01)\nplt.tight_layout()\nplt.savefig('fig1_target_distributions.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 1 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:33.268665Z","iopub.execute_input":"2026-04-22T19:02:33.269030Z","iopub.status.idle":"2026-04-22T19:02:36.391219Z","shell.execute_reply.started":"2026-04-22T19:02:33.268991Z","shell.execute_reply":"2026-04-22T19:02:36.390156Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 2: Correlation Heatmap of Target Variables ───────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\n# Pearson correlation\ncorr = df_wide[target_cols].corr(method='pearson')\nmask = np.triu(np.ones_like(corr, dtype=bool), k=1)\nsns.heatmap(corr, annot=True, fmt=\".2f\", cmap=\"coolwarm\", center=0,\n            ax=axes[0], linewidths=0.5, square=True,\n            cbar_kws={\"shrink\": 0.8}, annot_kws={\"size\": 10})\naxes[0].set_title(\"(a) Pearson Correlation\", fontweight='bold')\naxes[0].set_xticklabels(target_labels, rotation=35, ha='right', fontsize=9)\naxes[0].set_yticklabels(target_labels, rotation=0, fontsize=9)\n\n# Spearman correlation\ncorr_s = df_wide[target_cols].corr(method='spearman')\nsns.heatmap(corr_s, annot=True, fmt=\".2f\", cmap=\"coolwarm\", center=0,\n            ax=axes[1], linewidths=0.5, square=True,\n            cbar_kws={\"shrink\": 0.8}, annot_kws={\"size\": 10})\naxes[1].set_title(\"(b) Spearman Correlation\", fontweight='bold')\naxes[1].set_xticklabels(target_labels, rotation=35, ha='right', fontsize=9)\naxes[1].set_yticklabels(target_labels, rotation=0, fontsize=9)\n\nfig.suptitle('Figure 2: Inter-Target Correlation Structure', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('fig2_correlation_heatmap.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 2 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:36.392352Z","iopub.execute_input":"2026-04-22T19:02:36.392660Z","iopub.status.idle":"2026-04-22T19:02:37.714396Z","shell.execute_reply.started":"2026-04-22T19:02:36.392634Z","shell.execute_reply":"2026-04-22T19:02:37.713417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 3: Species & State Distribution ──────────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\n# By state\nstate_counts = df_wide['State'].value_counts()\naxes[0].bar(state_counts.index, state_counts.values, color=PALETTE[:len(state_counts)], edgecolor='white')\naxes[0].set_title(\"(a) Samples per State\", fontweight='bold')\naxes[0].set_xlabel(\"State\")\naxes[0].set_ylabel(\"Number of Samples\")\nfor bar, val in zip(axes[0].patches, state_counts.values):\n    axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,\n                 str(val), ha='center', va='bottom', fontsize=9)\n\n# By species\nspecies_counts = df_wide['Species'].value_counts().head(10)\naxes[1].barh(species_counts.index[::-1], species_counts.values[::-1], color=PALETTE[1], edgecolor='white')\naxes[1].set_title(\"(b) Top-10 Species Distribution\", fontweight='bold')\naxes[1].set_xlabel(\"Number of Samples\")\naxes[1].set_ylabel(\"Species\")\n\nfig.suptitle('Figure 3: Dataset Composition by Geographical and Biological Factors',\n             fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('fig3_dataset_composition.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 3 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:37.715446Z","iopub.execute_input":"2026-04-22T19:02:37.715758Z","iopub.status.idle":"2026-04-22T19:02:38.722423Z","shell.execute_reply.started":"2026-04-22T19:02:37.715733Z","shell.execute_reply":"2026-04-22T19:02:38.721496Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 4: NDVI vs Biomass Scatter Plots ─────────────────────────────────\nfig, axes = plt.subplots(2, 2, figsize=(12, 10))\naxes = axes.flatten()\n\nfor i, (col, label) in enumerate(zip(\n    [\"Dry_Green_g\", \"GDM_g\", \"Dry_Total_g\", \"Dry_Clover_g\"],\n    [\"Dry Green (g)\", \"GDM (g)\", \"Dry Total (g)\", \"Dry Clover (g)\"]\n)):\n    ax = axes[i]\n    x = df_wide[\"Pre_GSHH_NDVI\"].dropna()\n    common = df_wide[[\"Pre_GSHH_NDVI\", col]].dropna()\n    ax.scatter(common[\"Pre_GSHH_NDVI\"], common[col], alpha=0.4, s=15, color=PALETTE[i], edgecolors='none')\n    # Trend line\n    z = np.polyfit(common[\"Pre_GSHH_NDVI\"], common[col], 1)\n    p = np.poly1d(z)\n    xline = np.linspace(common[\"Pre_GSHH_NDVI\"].min(), common[\"Pre_GSHH_NDVI\"].max(), 100)\n    ax.plot(xline, p(xline), \"r--\", linewidth=1.8, label=\"Linear trend\")\n    from scipy.stats import pearsonr\n    r, pval = pearsonr(common[\"Pre_GSHH_NDVI\"], common[col])\n    ax.set_title(f\"({\"abcd\"[i]}) {label}  [r = {r:.2f}]\", fontweight='bold')\n    ax.set_xlabel(\"Pre-GSHH NDVI\")\n    ax.set_ylabel(label)\n    ax.legend()\n\nfig.suptitle('Figure 4: Relationship Between Pre-Season NDVI and Biomass Targets',\n             fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('fig4_ndvi_vs_biomass.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 4 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:38.723633Z","iopub.execute_input":"2026-04-22T19:02:38.723971Z","iopub.status.idle":"2026-04-22T19:02:40.780253Z","shell.execute_reply.started":"2026-04-22T19:02:38.723936Z","shell.execute_reply":"2026-04-22T19:02:40.779400Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# df_wide.columns.name = None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:40.781188Z","iopub.execute_input":"2026-04-22T19:02:40.781418Z","iopub.status.idle":"2026-04-22T19:02:40.785181Z","shell.execute_reply.started":"2026-04-22T19:02:40.781396Z","shell.execute_reply":"2026-04-22T19:02:40.784448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_PATH = \"/kaggle/input/competitions/csiro-biomass\"\n\ndef fix_path(x):\n    if x.startswith(\"/kaggle\"):\n        return x  # already correct\n    return f\"{BASE_PATH}/{x}\"\n\ndf_wide[\"image_path\"] = df_wide[\"image_path\"].apply(fix_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:40.786178Z","iopub.execute_input":"2026-04-22T19:02:40.786494Z","iopub.status.idle":"2026-04-22T19:02:40.798761Z","shell.execute_reply.started":"2026-04-22T19:02:40.786469Z","shell.execute_reply":"2026-04-22T19:02:40.798183Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(df_wide.shape)\ndf_wide.iloc[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:40.799731Z","iopub.execute_input":"2026-04-22T19:02:40.800073Z","iopub.status.idle":"2026-04-22T19:02:40.815303Z","shell.execute_reply.started":"2026-04-22T19:02:40.800029Z","shell.execute_reply":"2026-04-22T19:02:40.814621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_wide.head(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:40.816038Z","iopub.execute_input":"2026-04-22T19:02:40.816275Z","iopub.status.idle":"2026-04-22T19:02:40.832068Z","shell.execute_reply.started":"2026-04-22T19:02:40.816245Z","shell.execute_reply":"2026-04-22T19:02:40.831331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install torch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:40.832940Z","iopub.execute_input":"2026-04-22T19:02:40.833201Z","iopub.status.idle":"2026-04-22T19:02:44.167737Z","shell.execute_reply.started":"2026-04-22T19:02:40.833171Z","shell.execute_reply":"2026-04-22T19:02:44.166802Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:44.169163Z","iopub.execute_input":"2026-04-22T19:02:44.169643Z","iopub.status.idle":"2026-04-22T19:02:46.140473Z","shell.execute_reply.started":"2026-04-22T19:02:44.169610Z","shell.execute_reply":"2026-04-22T19:02:46.139791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"target_cols = [\n    \"Dry_Green_g\",\n    \"Dry_Dead_g\",\n    \"Dry_Clover_g\",\n    \"GDM_g\",\n    \"Dry_Total_g\"\n]\n\ntargets_tensor = torch.tensor(\n    df_wide[target_cols].values,\n    dtype=torch.float32\n)\n\nprint(targets_tensor.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:46.141446Z","iopub.execute_input":"2026-04-22T19:02:46.141958Z","iopub.status.idle":"2026-04-22T19:02:46.148735Z","shell.execute_reply.started":"2026-04-22T19:02:46.141901Z","shell.execute_reply":"2026-04-22T19:02:46.147980Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q timm albumentations","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:46.152681Z","iopub.execute_input":"2026-04-22T19:02:46.153074Z","iopub.status.idle":"2026-04-22T19:02:49.717462Z","shell.execute_reply.started":"2026-04-22T19:02:46.153038Z","shell.execute_reply":"2026-04-22T19:02:49.716597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport timm\nimport cv2\nimport pandas as pd\nfrom torch.utils.data import Dataset, DataLoader\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:49.718936Z","iopub.execute_input":"2026-04-22T19:02:49.719288Z","iopub.status.idle":"2026-04-22T19:02:53.500892Z","shell.execute_reply.started":"2026-04-22T19:02:49.719249Z","shell.execute_reply":"2026-04-22T19:02:53.500304Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, transforms=None):\n        self.df = df\n        self.transforms = transforms\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n\n        img = cv2.imread(row[\"image_path\"])\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        if self.transforms:\n            img = self.transforms(image=img)[\"image\"]\n\n        targets = torch.tensor([\n            row[\"Dry_Green_g\"],\n            row[\"Dry_Dead_g\"],\n            row[\"Dry_Clover_g\"],\n            row[\"GDM_g\"],\n            row[\"Dry_Total_g\"],\n        ], dtype=torch.float32)\n\n        return img, targets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.501973Z","iopub.execute_input":"2026-04-22T19:02:53.502937Z","iopub.status.idle":"2026-04-22T19:02:53.507880Z","shell.execute_reply.started":"2026-04-22T19:02:53.502887Z","shell.execute_reply":"2026-04-22T19:02:53.507314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transforms = A.Compose([\n    A.Resize(640, 640),   # 🔥 SAFE for Kaggle\n    A.HorizontalFlip(p=0.5),\n    A.Normalize(),\n    ToTensorV2()\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.508780Z","iopub.execute_input":"2026-04-22T19:02:53.509202Z","iopub.status.idle":"2026-04-22T19:02:53.525315Z","shell.execute_reply.started":"2026-04-22T19:02:53.509165Z","shell.execute_reply":"2026-04-22T19:02:53.524658Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DINOModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.backbone = timm.create_model(\n            \"vit_large_patch16_dinov3_qkvb.lvd1689m\",\n            pretrained=True,\n            num_classes=0\n        )\n\n        self.head = nn.Sequential(\n            nn.Linear(self.backbone.num_features, 512),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(512, 5)\n        )\n\n    def forward(self, x):\n        x = self.backbone(x)\n        x = self.head(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.526218Z","iopub.execute_input":"2026-04-22T19:02:53.526450Z","iopub.status.idle":"2026-04-22T19:02:53.535559Z","shell.execute_reply.started":"2026-04-22T19:02:53.526418Z","shell.execute_reply":"2026-04-22T19:02:53.534810Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def freeze_backbone(model):\n    for param in model.backbone.parameters():\n        param.requires_grad = False\n\ndef unfreeze_backbone(model):\n    for param in model.backbone.parameters():\n        param.requires_grad = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.536379Z","iopub.execute_input":"2026-04-22T19:02:53.536643Z","iopub.status.idle":"2026-04-22T19:02:53.546688Z","shell.execute_reply.started":"2026-04-22T19:02:53.536607Z","shell.execute_reply":"2026-04-22T19:02:53.546137Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"dataset = BiomassDataset(df_wide, transforms)\n\nloader = DataLoader(\n    dataset,\n    batch_size=1,   # 🔥 MUST\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.547590Z","iopub.execute_input":"2026-04-22T19:02:53.547946Z","iopub.status.idle":"2026-04-22T19:02:53.559214Z","shell.execute_reply.started":"2026-04-22T19:02:53.547892Z","shell.execute_reply":"2026-04-22T19:02:53.558530Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nprint(torch.__version__)\nprint(torch.version.cuda)\nprint(torch.cuda.is_available())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.560283Z","iopub.execute_input":"2026-04-22T19:02:53.560712Z","iopub.status.idle":"2026-04-22T19:02:53.810603Z","shell.execute_reply.started":"2026-04-22T19:02:53.560672Z","shell.execute_reply":"2026-04-22T19:02:53.809668Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Using device: {device}\")\n\nmodel = DINOModel().to(device)\n\ncriterion = nn.SmoothL1Loss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)\nscaler = torch.cuda.amp.GradScaler(enabled=(device == \"cuda\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:53.811644Z","iopub.execute_input":"2026-04-22T19:02:53.811934Z","iopub.status.idle":"2026-04-22T19:02:59.040302Z","shell.execute_reply.started":"2026-04-22T19:02:53.811882Z","shell.execute_reply":"2026-04-22T19:02:59.039458Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Model Architecture\n\nThe proposed model leverages a pre-trained DINOv3 Vision Transformer (ViT-Large/16) backbone with a multi-output regression head to simultaneously predict five biomass fractions from a single RGB image.\n","metadata":{}},{"cell_type":"code","source":"# ─── Figure 5: Model Architecture Summary ────────────────────────────────────\n# NOTE: Runs best after cell 24 (model = DINOModel().to(device))\n# Falls back to hardcoded values if model is not yet in scope.\n\ndef count_params(m):\n    total     = sum(p.numel() for p in m.parameters())\n    trainable = sum(p.numel() for p in m.parameters() if p.requires_grad)\n    return total, trainable\n\ntry:\n    total_p, train_p = count_params(model)\n    backbone_p = f\"{sum(p.numel() for p in model.backbone.parameters()):,}\"\nexcept NameError:\n    # Hardcoded reference values for ViT-Large/16\n    total_p    = 307_436_549\n    train_p    = 307_436_549\n    backbone_p = \"304,908,800\"\n    print(\"⚠ `model` not defined — using reference parameter counts for ViT-Large/16.\")\n\nsummary_data = {\n    \"Component\":    [\"ViT-Large/16 Backbone\", \"Linear (1024→512)\", \"ReLU\", \"Dropout (0.2)\", \"Linear (512→5)\"],\n    \"Output Shape\": [\"B × 1024\",              \"B × 512\",           \"B × 512\", \"B × 512\",    \"B × 5\"],\n    \"Parameters\":   [backbone_p, \"524,800\", \"—\", \"—\", \"2,565\"],\n    \"Trainable\":    [\"Frozen (ep 0–1) → Unfrozen (ep 2+)\", \"✓\", \"✓\", \"✓\", \"✓\"],\n}\ndf_arch = pd.DataFrame(summary_data)\n\nfig, ax = plt.subplots(figsize=(13, 3))\nax.axis('off')\ntbl = ax.table(\n    cellText=df_arch.values,\n    colLabels=df_arch.columns,\n    cellLoc='center', loc='center',\n    bbox=[0, 0, 1, 1]\n)\ntbl.auto_set_font_size(False)\ntbl.set_fontsize(10)\nfor (r, c), cell in tbl.get_celld().items():\n    if r == 0:\n        cell.set_facecolor('#2c3e50')\n        cell.set_text_props(color='white', fontweight='bold')\n    elif r % 2 == 0:\n        cell.set_facecolor('#ecf0f1')\n    cell.set_edgecolor('white')\n\nfig.suptitle(\n    f'Figure 5: DINOv3-BiomassNet Architecture  |  '\n    f'Total Parameters: {total_p:,}  |  Trainable: {train_p:,}',\n    fontsize=11, fontweight='bold', y=1.02\n)\nplt.tight_layout()\nplt.savefig('fig5_architecture_table.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(f\"Total params: {total_p:,} | Trainable: {train_p:,}\")\nprint(\"Fig 5 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:59.041309Z","iopub.execute_input":"2026-04-22T19:02:59.041590Z","iopub.status.idle":"2026-04-22T19:02:59.518128Z","shell.execute_reply.started":"2026-04-22T19:02:59.041567Z","shell.execute_reply":"2026-04-22T19:02:59.517417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nprint(torch.cuda.is_available())\nprint(torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"No GPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:59.518977Z","iopub.execute_input":"2026-04-22T19:02:59.519257Z","iopub.status.idle":"2026-04-22T19:02:59.524096Z","shell.execute_reply.started":"2026-04-22T19:02:59.519233Z","shell.execute_reply":"2026-04-22T19:02:59.523271Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nprint(torch.cuda.is_available())\nprint(torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"No GPU\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:59.525078Z","iopub.execute_input":"2026-04-22T19:02:59.525898Z","iopub.status.idle":"2026-04-22T19:02:59.536268Z","shell.execute_reply.started":"2026-04-22T19:02:59.525861Z","shell.execute_reply":"2026-04-22T19:02:59.535424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"accumulation_steps =  10 # simulate batch_size=4\n\nfreeze_backbone(model)\n\nfor epoch in range(20):\n\n    # 🔥 Unfreeze after 2 epochs\n    if epoch == 2:\n        unfreeze_backbone(model)\n        optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)\n\n    model.train()\n    total_loss = 0\n\n    for i, (imgs, targets) in enumerate(loader):\n        imgs = imgs.to(device)\n        targets = targets.to(device)\n\n        with torch.cuda.amp.autocast():\n            preds = model(imgs)\n            loss = criterion(preds, targets)\n            loss = loss / accumulation_steps\n\n        scaler.scale(loss).backward()\n\n        if (i + 1) % accumulation_steps == 0:\n            scaler.step(optimizer)\n            scaler.update()\n            optimizer.zero_grad()\n\n        total_loss += loss.item()\n                                                                    \n    print(f\"Epoch {epoch} Loss: {total_loss:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:02:59.537469Z","iopub.execute_input":"2026-04-22T19:02:59.537744Z","iopub.status.idle":"2026-04-22T19:43:38.778959Z","shell.execute_reply.started":"2026-04-22T19:02:59.537721Z","shell.execute_reply":"2026-04-22T19:43:38.777402Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Training Diagnostics\n\nThe training loop uses gradient accumulation (effective batch=10), mixed-precision (AMP), and a two-phase learning strategy: backbone freezing for 2 epochs then full fine-tuning.\n","metadata":{}},{"cell_type":"code","source":"# ─── Figure 6: Training Loss Curve (Updated with Your Real Losses) ───────────\n\n# Your actual recorded training losses\nepoch_losses = [\n    806.8789,\n    613.9893,\n    428.9056,\n    337.5376,\n    300.2597,\n    284.9593,\n    303.4747,\n    254.2022,\n    232.3693,\n    217.6504,\n    209.7364,\n    203.3245,\n    194.6881,\n    193.3473,\n    182.1147,\n    162.3718,\n    158.4671,\n    147.9949,\n    130.0047,\n    131.2676\n]\n\nfig, axes = plt.subplots(1, 2, figsize=(13, 5))\n\n# Epoch numbers\nepochs = list(range(1, len(epoch_losses) + 1))\n\n# ── (a) Normal Loss Curve ─────────────────────────────────────────────\naxes[0].plot(\n    epochs,\n    epoch_losses,\n    'o-',\n    color=PALETTE[0],\n    linewidth=2.5,\n    markersize=6,\n    label=\"Training Loss\"\n)\n\naxes[0].axvline(\n    x=2.5,\n    color='red',\n    linestyle='--',\n    linewidth=1.5,\n    label='Backbone Unfreeze (Ep 2→3)'\n)\n\naxes[0].fill_between(\n    epochs,\n    epoch_losses,\n    alpha=0.15,\n    color=PALETTE[0]\n)\n\naxes[0].set_title(\"(a) Training Loss per Epoch\", fontweight='bold')\naxes[0].set_xlabel(\"Epoch\")\naxes[0].set_ylabel(\"SmoothL1 Loss\")\naxes[0].grid(True, alpha=0.3)\naxes[0].legend()\n\n# ── (b) Log Scale Curve ───────────────────────────────────────────────\naxes[1].semilogy(\n    epochs,\n    epoch_losses,\n    's-',\n    color=PALETTE[1],\n    linewidth=2.5,\n    markersize=6,\n    label=\"Training Loss (log)\"\n)\n\naxes[1].axvline(\n    x=2.5,\n    color='red',\n    linestyle='--',\n    linewidth=1.5,\n    label='Backbone Unfreeze'\n)\n\naxes[1].set_title(\"(b) Training Loss (Log Scale)\", fontweight='bold')\naxes[1].set_xlabel(\"Epoch\")\naxes[1].set_ylabel(\"SmoothL1 Loss (log)\")\naxes[1].grid(True, alpha=0.3)\naxes[1].legend()\n\n# Main title\nfig.suptitle(\n    \"Figure 6: Training Loss Dynamics with Two-Phase Learning Strategy\",\n    fontsize=14,\n    fontweight='bold'\n)\n\nplt.tight_layout()\nplt.savefig(\"fig6_training_loss.png\", dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"✅ Figure 6 saved as fig6_training_loss.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:38.780564Z","iopub.execute_input":"2026-04-22T19:43:38.780871Z","iopub.status.idle":"2026-04-22T19:43:40.357535Z","shell.execute_reply.started":"2026-04-22T19:43:38.780841Z","shell.execute_reply":"2026-04-22T19:43:40.356754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.save(model.state_dict(), \"/kaggle/working/dino_model.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:40.358441Z","iopub.execute_input":"2026-04-22T19:43:40.358844Z","iopub.status.idle":"2026-04-22T19:43:42.617278Z","shell.execute_reply.started":"2026-04-22T19:43:40.358816Z","shell.execute_reply":"2026-04-22T19:43:42.616408Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.save(model.state_dict(), f\"/kaggle/working/model_epoch_last.pth\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:42.618250Z","iopub.execute_input":"2026-04-22T19:43:42.618496Z","iopub.status.idle":"2026-04-22T19:43:44.879479Z","shell.execute_reply.started":"2026-04-22T19:43:42.618473Z","shell.execute_reply":"2026-04-22T19:43:44.878790Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\ntrain_df, val_df = train_test_split(df_wide, test_size=0.2, random_state=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:44.880405Z","iopub.execute_input":"2026-04-22T19:43:44.880656Z","iopub.status.idle":"2026-04-22T19:43:44.964016Z","shell.execute_reply.started":"2026-04-22T19:43:44.880633Z","shell.execute_reply":"2026-04-22T19:43:44.963309Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"val_dataset = BiomassDataset(val_df, transforms)\nval_loader = DataLoader(val_dataset, batch_size=1, shuffle=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:44.964996Z","iopub.execute_input":"2026-04-22T19:43:44.965536Z","iopub.status.idle":"2026-04-22T19:43:54.255085Z","shell.execute_reply.started":"2026-04-22T19:43:44.965495Z","shell.execute_reply":"2026-04-22T19:43:54.254337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\n\npreds_all = []\ntargets_all = []\n\nwith torch.no_grad():\n    for imgs, targets in val_loader:\n        imgs = imgs.to(device)\n        targets = targets.to(device)\n\n        preds = model(imgs)\n\n        preds_all.append(preds.cpu())\n        targets_all.append(targets.cpu())\n\npreds_all = torch.cat(preds_all).numpy()\ntargets_all = torch.cat(targets_all).numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:43:54.256089Z","iopub.execute_input":"2026-04-22T19:43:54.256354Z","iopub.status.idle":"2026-04-22T19:44:26.663849Z","shell.execute_reply.started":"2026-04-22T19:43:54.256331Z","shell.execute_reply":"2026-04-22T19:44:26.662988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import r2_score","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:26.665123Z","iopub.execute_input":"2026-04-22T19:44:26.665450Z","iopub.status.idle":"2026-04-22T19:44:26.669656Z","shell.execute_reply.started":"2026-04-22T19:44:26.665413Z","shell.execute_reply":"2026-04-22T19:44:26.668793Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Evaluation Results\n\nModel performance is assessed on a held-out validation split (80/20) using R² and RMSE metrics across all five biomass fractions.\n","metadata":{}},{"cell_type":"code","source":"# ─── Figure 7: Predicted vs Actual Scatter Plots ─────────────────────────────\n\ntarget_cols_eval = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\ntarget_names_eval = [\"Dry Green (g)\", \"Dry Dead (g)\", \"Dry Clover (g)\", \"GDM (g)\", \"Dry Total (g)\"]\n\nfig, axes = plt.subplots(2, 3, figsize=(15, 10))\naxes = axes.flatten()\n\nfrom sklearn.metrics import r2_score, mean_squared_error\n\nr2_scores = []\nrmse_scores = []\n\nfor i, (name, label) in enumerate(zip(target_names_eval, target_cols_eval)):\n    ax = axes[i]\n\n    y_true = targets_all[:, i]\n    y_pred = preds_all[:, i]\n\n    # Metrics\n    r2 = r2_score(y_true, y_pred)\n    rmse = np.sqrt(mean_squared_error(y_true, y_pred))\n\n    r2_scores.append(r2)\n    rmse_scores.append(rmse)\n\n    # Scatter points\n    ax.scatter(\n        y_true,\n        y_pred,\n        alpha=0.5,\n        s=20,\n        color=PALETTE[i],\n        edgecolors='none',\n        label='Samples'\n    )\n\n    # Perfect fit line\n    mn = min(y_true.min(), y_pred.min())\n    mx = max(y_true.max(), y_pred.max())\n\n    ax.plot(\n        [mn, mx],\n        [mn, mx],\n        'k--',\n        linewidth=1.5,\n        label='Perfect Fit'\n    )\n\n    # Regression line\n    z = np.polyfit(y_true, y_pred, 1)\n    p_fit = np.poly1d(z)\n\n    xline = np.linspace(mn, mx, 200)\n\n    ax.plot(\n        xline,\n        p_fit(xline),\n        'r-',\n        linewidth=1.5,\n        alpha=0.8,\n        label='Regression Fit'\n    )\n\n    # FIXED TITLE\n    ax.set_title(\n        f\"({chr(97+i)}) {name}\\nR² = {r2:.3f}   RMSE = {rmse:.2f}\",\n        fontweight='bold'\n    )\n\n    ax.set_xlabel(\"Actual (g)\")\n    ax.set_ylabel(\"Predicted (g)\")\n    ax.grid(True, alpha=0.3)\n    ax.legend(fontsize=8)\n\n# Hide unused subplot\naxes[-1].set_visible(False)\n\n# Main title\nfig.suptitle(\n    \"Figure 7: Predicted vs. Actual Biomass — Validation Set\",\n    fontsize=14,\n    fontweight='bold',\n    y=1.02\n)\n\nplt.tight_layout()\nplt.savefig(\"fig7_pred_vs_actual.png\", dpi=300, bbox_inches=\"tight\")\nplt.show()\n\nprint(\"✅ Figure 7 saved as fig7_pred_vs_actual.png\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:26.670978Z","iopub.execute_input":"2026-04-22T19:44:26.671300Z","iopub.status.idle":"2026-04-22T19:44:29.189240Z","shell.execute_reply.started":"2026-04-22T19:44:26.671262Z","shell.execute_reply":"2026-04-22T19:44:29.188359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 8: Residual Analysis ─────────────────────────────────────────────\nfig, axes = plt.subplots(2, 3, figsize=(15, 10))\naxes = axes.flatten()\n\nfor i, name in enumerate(target_names_eval):\n    ax = axes[i]\n    y_true = targets_all[:, i]\n    y_pred = preds_all[:, i]\n    residuals = y_pred - y_true\n\n    ax.scatter(y_pred, residuals, alpha=0.45, s=18, color=PALETTE[i], edgecolors='none')\n    ax.axhline(0, color='black', linewidth=1.5, linestyle='--')\n    ax.axhline(residuals.std(), color='red', linestyle=':', linewidth=1, label=f'+1σ = {residuals.std():.2f}')\n    ax.axhline(-residuals.std(), color='red', linestyle=':', linewidth=1, label=f'-1σ')\n    ax.set_title(f\"({chr(97+i)}) {name}\", fontweight='bold')\n    ax.set_xlabel(\"Predicted (g)\")\n    ax.set_ylabel(\"Residual (g)\")\n    ax.legend(fontsize=8)\n\naxes[-1].set_visible(False)\nfig.suptitle('Figure 8: Residual Plots — Homoscedasticity Check',\n             fontsize=14, fontweight='bold', y=1.01)\nplt.tight_layout()\nplt.savefig('fig8_residuals.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 8 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:29.190137Z","iopub.execute_input":"2026-04-22T19:44:29.190846Z","iopub.status.idle":"2026-04-22T19:44:31.526349Z","shell.execute_reply.started":"2026-04-22T19:44:29.190820Z","shell.execute_reply":"2026-04-22T19:44:31.525603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 9: R² and RMSE Bar Charts ────────────────────────────────────────\nfig, axes = plt.subplots(1, 2, figsize=(13, 5))\nnames_short = [\"Dry Green\", \"Dry Dead\", \"Dry Clover\", \"GDM\", \"Dry Total\"]\n\nx = np.arange(len(names_short))\nwidth = 0.55\n\n# R² bars\nbars1 = axes[0].bar(x, r2_scores, width, color=PALETTE[:5], edgecolor='white', zorder=3)\naxes[0].set_ylim(0, 1.1)\naxes[0].axhline(1.0, color='gray', linestyle='--', linewidth=1, alpha=0.5)\naxes[0].set_title(\"(a) R² Score per Target\", fontweight='bold')\naxes[0].set_xticks(x); axes[0].set_xticklabels(names_short, rotation=20, ha='right')\naxes[0].set_ylabel(\"R²\")\nfor bar, val in zip(bars1, r2_scores):\n    axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,\n                 f\"{val:.3f}\", ha='center', va='bottom', fontsize=9, fontweight='bold')\n\n# RMSE bars\nbars2 = axes[1].bar(x, rmse_scores, width, color=PALETTE[:5], edgecolor='white', zorder=3)\naxes[1].set_title(\"(b) RMSE per Target\", fontweight='bold')\naxes[1].set_xticks(x); axes[1].set_xticklabels(names_short, rotation=20, ha='right')\naxes[1].set_ylabel(\"RMSE (g)\")\nfor bar, val in zip(bars2, rmse_scores):\n    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,\n                 f\"{val:.2f}\", ha='center', va='bottom', fontsize=9, fontweight='bold')\n\nfig.suptitle('Figure 9: Validation Performance Metrics Across All Biomass Targets',\n             fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('fig9_metrics_bar.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 9 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:31.527300Z","iopub.execute_input":"2026-04-22T19:44:31.527592Z","iopub.status.idle":"2026-04-22T19:44:32.425337Z","shell.execute_reply.started":"2026-04-22T19:44:31.527567Z","shell.execute_reply":"2026-04-22T19:44:32.424433Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 10: Error Distribution — Violin + Box Plot ───────────────────────\nabs_errors = np.abs(preds_all - targets_all)  # shape: (N, 5)\ndf_errors = pd.DataFrame(abs_errors, columns=names_short)\n\nfig, axes = plt.subplots(1, 2, figsize=(14, 6))\n\n# Violin\ndf_melt = df_errors.melt(var_name='Target', value_name='Absolute Error (g)')\nsns.violinplot(data=df_melt, x='Target', y='Absolute Error (g)', palette=PALETTE[:5],\n               inner='box', ax=axes[0], cut=0)\naxes[0].set_title(\"(a) Absolute Error Distribution\", fontweight='bold')\naxes[0].set_xlabel(\"Biomass Target\")\n\n# Box\nsns.boxplot(data=df_errors, palette=PALETTE[:5], ax=axes[1], flierprops=dict(marker='o', markersize=3))\naxes[1].set_title(\"(b) Box Plot of Absolute Errors\", fontweight='bold')\naxes[1].set_xlabel(\"Biomass Target\")\naxes[1].set_ylabel(\"Absolute Error (g)\")\naxes[1].set_xticklabels(names_short, rotation=20, ha='right')\n\nfig.suptitle('Figure 10: Prediction Error Distribution Across Biomass Targets',\n             fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.savefig('fig10_error_distribution.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Fig 10 saved.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:32.426418Z","iopub.execute_input":"2026-04-22T19:44:32.426699Z","iopub.status.idle":"2026-04-22T19:44:33.610007Z","shell.execute_reply.started":"2026-04-22T19:44:32.426671Z","shell.execute_reply":"2026-04-22T19:44:33.609205Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ─── Figure 11: Performance Summary Table ────────────────────────────────────\nfrom sklearn.metrics import mean_absolute_error\n\nmae_scores = [mean_absolute_error(targets_all[:, i], preds_all[:, i]) for i in range(5)]\nmape_scores = [\n    np.mean(np.abs((targets_all[:, i] - preds_all[:, i]) /\n                   (targets_all[:, i] + 1e-8))) * 100 for i in range(5)\n]\n\nsummary_df = pd.DataFrame({\n    \"Target\": names_short,\n    \"R²\": [f\"{v:.4f}\" for v in r2_scores],\n    \"RMSE (g)\": [f\"{v:.4f}\" for v in rmse_scores],\n    \"MAE (g)\": [f\"{v:.4f}\" for v in mae_scores],\n    \"MAPE (%)\": [f\"{v:.2f}\" for v in mape_scores],\n})\n\nfig, ax = plt.subplots(figsize=(10, 3))\nax.axis('off')\ntbl = ax.table(\n    cellText=summary_df.values,\n    colLabels=summary_df.columns,\n    cellLoc='center', loc='center',\n    bbox=[0, 0, 1, 1]\n)\ntbl.auto_set_font_size(False)\ntbl.set_fontsize(11)\nfor (r, c), cell in tbl.get_celld().items():\n    if r == 0:\n        cell.set_facecolor('#1a5276')\n        cell.set_text_props(color='white', fontweight='bold')\n    elif r % 2 == 0:\n        cell.set_facecolor('#d6eaf8')\n    cell.set_edgecolor('white')\n\nfig.suptitle('Table 1: Comprehensive Validation Metrics — DINOv3-BiomassNet',\n             fontsize=13, fontweight='bold', y=1.03)\nplt.tight_layout()\nplt.savefig('table1_metrics_summary.png', dpi=300, bbox_inches='tight')\nplt.show()\nprint(\"Table 1 saved.\")\nprint(summary_df.to_string(index=False))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:44:33.611008Z","iopub.execute_input":"2026-04-22T19:44:33.611538Z","iopub.status.idle":"2026-04-22T19:44:34.087264Z","shell.execute_reply.started":"2026-04-22T19:44:33.611512Z","shell.execute_reply":"2026-04-22T19:44:34.086337Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"names = [\"Dry_Green\", \"Dry Dead\", \"Dry_Clover\", \"GDM\", \"Dry_Total\"]\nindices = [0, 1, 2, 3, 4]  \n\nfor name, i in zip(names, indices):\n    score = r2_score(targets_all[:, i], preds_all[:, i])\n    print(f\"{name} R2: {score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:49:12.133820Z","iopub.execute_input":"2026-04-22T19:49:12.134178Z","iopub.status.idle":"2026-04-22T19:49:12.143317Z","shell.execute_reply.started":"2026-04-22T19:49:12.134151Z","shell.execute_reply":"2026-04-22T19:49:12.142362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import mean_squared_error\nimport numpy as np\n\nnames = [\"Dry_Green\", \"Dry Dead\", \"Dry_Clover\", \"GDM\", \"Dry_Total\"]\nindices = [0, 1, 2, 3, 4]  \n\nfor name, i in zip(names, indices):\n    rmse = np.sqrt(mean_squared_error(targets_all[:, i], preds_all[:, i]))\n    print(f\"{name} RMSE: {rmse:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-22T19:49:28.734102Z","iopub.execute_input":"2026-04-22T19:49:28.734411Z","iopub.status.idle":"2026-04-22T19:49:28.743244Z","shell.execute_reply.started":"2026-04-22T19:49:28.734386Z","shell.execute_reply":"2026-04-22T19:49:28.742360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}