{"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":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport math\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\n\nfrom tqdm import tqdm","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:09.275804Z","iopub.execute_input":"2026-01-10T07:25:09.276155Z","iopub.status.idle":"2026-01-10T07:25:16.403305Z","shell.execute_reply.started":"2026-01-10T07:25:09.276127Z","shell.execute_reply":"2026-01-10T07:25:16.402653Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if torch.cuda.is_available():\n    device = \"cuda:0\"\nelse:\n    device = \"cpu\"\ndevice = torch.device(device)\n\nprint(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:16.404427Z","iopub.execute_input":"2026-01-10T07:25:16.404832Z","iopub.status.idle":"2026-01-10T07:25:16.493552Z","shell.execute_reply.started":"2026-01-10T07:25:16.404806Z","shell.execute_reply":"2026-01-10T07:25:16.492732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# paths\nDATA_DIR = \"/kaggle/input/csiro-biomass\"\nCSV_PATH = \"/kaggle/input/csiro-biomass/train.csv\"\n# read labels\ndf = pd.read_csv(CSV_PATH)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:16.494372Z","iopub.execute_input":"2026-01-10T07:25:16.494663Z","iopub.status.idle":"2026-01-10T07:25:16.532987Z","shell.execute_reply.started":"2026-01-10T07:25:16.494624Z","shell.execute_reply":"2026-01-10T07:25:16.532406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"newdf = df[['sample_id','image_path','target_name','target']]\nnewdf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:16.534353Z","iopub.execute_input":"2026-01-10T07:25:16.53465Z","iopub.status.idle":"2026-01-10T07:25:16.563948Z","shell.execute_reply.started":"2026-01-10T07:25:16.534627Z","shell.execute_reply":"2026-01-10T07:25:16.563382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# basic image transform\ntransform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(\n        [0.485, 0.456, 0.406],\n        [0.229, 0.224, 0.225]\n    )\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:16.56481Z","iopub.execute_input":"2026-01-10T07:25:16.565092Z","iopub.status.idle":"2026-01-10T07:25:16.569608Z","shell.execute_reply.started":"2026-01-10T07:25:16.565058Z","shell.execute_reply":"2026-01-10T07:25:16.568946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"newdf['target_name']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:17.356032Z","iopub.execute_input":"2026-01-10T07:25:17.356381Z","iopub.status.idle":"2026-01-10T07:25:17.363582Z","shell.execute_reply.started":"2026-01-10T07:25:17.356351Z","shell.execute_reply":"2026-01-10T07:25:17.3629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None):\n        self.df = df\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_path = os.path.join(self.img_dir, self.df.iloc[idx][\"image_path\"])\n        image = Image.open(img_path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        label = self.df.iloc[idx][\"target_name\"]\n        val = self.df.iloc[idx][\"target\"]\n        return image, label,val","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:19.141117Z","iopub.execute_input":"2026-01-10T07:25:19.141449Z","iopub.status.idle":"2026-01-10T07:25:19.14742Z","shell.execute_reply.started":"2026-01-10T07:25:19.141423Z","shell.execute_reply":"2026-01-10T07:25:19.146627Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.ToTensor()\ndataset = BiomassDataset(newdf, DATA_DIR, transform=transform)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:46.713675Z","iopub.execute_input":"2026-01-10T07:25:46.71397Z","iopub.status.idle":"2026-01-10T07:25:46.718383Z","shell.execute_reply.started":"2026-01-10T07:25:46.713943Z","shell.execute_reply":"2026-01-10T07:25:46.717144Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, axes = plt.subplots(1, 5, figsize=(15, 4))\n\nfor i in range(5):\n    img, label,val = dataset[i]\n    axes[i].imshow(img.permute(1, 2, 0))  # tensor → HWC\n    axes[i].set_title(f\"Biomass: {label}\\nTarget Value: {val}\")\n    axes[i].axis(\"off\")\n\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:48.956336Z","iopub.execute_input":"2026-01-10T07:25:48.956987Z","iopub.status.idle":"2026-01-10T07:25:50.294799Z","shell.execute_reply.started":"2026-01-10T07:25:48.956958Z","shell.execute_reply":"2026-01-10T07:25:50.293999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BiomassDataset(Dataset):\n    def __init__(self, df, img_dir, transform=None):\n        self.df = df.reset_index(drop=True)\n        self.img_dir = img_dir\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        img_path = os.path.join(\n            self.img_dir,\n            os.path.basename(self.df.iloc[idx][\"image_path\"])\n        )\n\n        image = Image.open(img_path).convert(\"RGB\")\n\n        if self.transform:\n            image = self.transform(image)\n\n        label = self.df.iloc[idx][\"target_name\"]\n        value = self.df.iloc[idx][\"target\"]\n\n        return image, label, value\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:25:54.264584Z","iopub.execute_input":"2026-01-10T07:25:54.264897Z","iopub.status.idle":"2026-01-10T07:25:54.270697Z","shell.execute_reply.started":"2026-01-10T07:25:54.264871Z","shell.execute_reply":"2026-01-10T07:25:54.269833Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = (224,224)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:29:47.965298Z","iopub.execute_input":"2026-01-10T07:29:47.965674Z","iopub.status.idle":"2026-01-10T07:29:47.970105Z","shell.execute_reply.started":"2026-01-10T07:29:47.965643Z","shell.execute_reply":"2026-01-10T07:29:47.969106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"transform = transforms.Compose([\n    transforms.Resize(IMG_SIZE),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:29:49.998133Z","iopub.execute_input":"2026-01-10T07:29:49.998494Z","iopub.status.idle":"2026-01-10T07:29:50.003513Z","shell.execute_reply.started":"2026-01-10T07:29:49.998466Z","shell.execute_reply":"2026-01-10T07:29:50.00269Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\n# Split dataframe into train and test (e.g., 80-20 split)\ndf_train, df_test = train_test_split(df, test_size=0.2, random_state=42, shuffle=True)\n\n# Create dataset instances\ntrain_dataset = BiomassDataset(\n    df_train,\n    img_dir=\"/kaggle/input/csiro-biomass/train\",\n    transform=transform,\n    #patch_size=224,\n    #grid_size=1\n)\n\ntest_dataset = BiomassDataset(\n    df_test,\n    img_dir=\"/kaggle/input/csiro-biomass/train\",\n    transform=transform,\n    #patch_size=224,\n    #grid_size=1\n)\n# Optional: check number of patches\nprint(\"Train patches:\", len(train_dataset))\nprint(\"Test patches:\", len(test_dataset))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:29:51.979405Z","iopub.execute_input":"2026-01-10T07:29:51.979719Z","iopub.status.idle":"2026-01-10T07:29:51.987855Z","shell.execute_reply.started":"2026-01-10T07:29:51.979691Z","shell.execute_reply":"2026-01-10T07:29:51.98712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)\ntest_loader  = DataLoader(test_dataset, batch_size=16, shuffle=False)\nfor images, labels, values in train_loader:\n    print(f\"Batch images shape: {images.shape}\")   # (16, 3, 224, 224)\n    print(f\"Labels type:\", type(labels))           # usually list of strings\n    print(f\"Values shape:\", values.shape)          # (16,)\n    break\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:29:53.681591Z","iopub.execute_input":"2026-01-10T07:29:53.681931Z","iopub.status.idle":"2026-01-10T07:29:54.971886Z","shell.execute_reply.started":"2026-01-10T07:29:53.681895Z","shell.execute_reply":"2026-01-10T07:29:54.971002Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nfrom sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error\nfrom tqdm import tqdm\nimport math\n\n# Global lists for storing predictions for later plotting\ny_pred, y_true = [], []\n\ndef regression_accuracy(y_true, y_pred, tolerance):\n    \"\"\"\n    Percentage of predictions within ±tolerance\n    \"\"\"\n    return (abs(y_true - y_pred) <= tolerance).mean()\n\ndef train_and_evaluate(\n    model,\n    train_loader,\n    test_loader,\n    num_epochs=3,\n    lr=1e-4,\n    tolerance=10.0,   # <-- CHANGE THIS BASED ON BIOMASS SCALE\n    device=None\n):\n    global y_pred, y_true\n\n    if device is None:\n        device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    model = model.to(device)\n    criterion = nn.MSELoss()\n    optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n\n    metrics = {\n        \"train_rmse\": [], \"train_mae\": [], \"train_r2\": [], \"train_acc\": [],\n        \"test_rmse\": [],  \"test_mae\": [],  \"test_r2\": [],  \"test_acc\": []\n    }\n\n    for epoch in range(num_epochs):\n\n        # =====================\n        # TRAIN\n        # =====================\n        model.train()\n        train_preds, train_targets = [], []\n\n        train_bar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Train]\")\n\n        for images, _, values in train_bar:\n            images = images.to(device)\n            values = values.to(device).float()\n\n            optimizer.zero_grad()\n            outputs = model(images)\n\n            loss = criterion(outputs, values)\n            loss.backward()\n            optimizer.step()\n\n            train_preds.append(outputs.detach().cpu())\n            train_targets.append(values.detach().cpu())\n\n            train_bar.set_postfix(loss=loss.item())\n\n        train_preds = torch.cat(train_preds).numpy()\n        train_targets = torch.cat(train_targets).numpy()\n\n        train_rmse = math.sqrt(mean_squared_error(train_targets, train_preds))\n        train_mae = mean_absolute_error(train_targets, train_preds)\n        train_r2 = r2_score(train_targets, train_preds)\n        train_acc = regression_accuracy(train_targets, train_preds, tolerance)\n\n        metrics[\"train_rmse\"].append(train_rmse)\n        metrics[\"train_mae\"].append(train_mae)\n        metrics[\"train_r2\"].append(train_r2)\n        metrics[\"train_acc\"].append(train_acc)\n\n        # =====================\n        # TEST\n        # =====================\n        model.eval()\n        test_preds, test_targets = [], []\n\n        test_bar = tqdm(test_loader, desc=f\"Epoch {epoch+1}/{num_epochs} [Test]\")\n\n        with torch.no_grad():\n            for images, _, values in test_bar:\n                images = images.to(device)\n                values = values.to(device).float()\n\n                outputs = model(images)\n\n                test_preds.append(outputs.cpu())\n                test_targets.append(values.cpu())\n\n        # Combine all batch predictions\n        test_preds = torch.cat(test_preds).numpy()\n        test_targets = torch.cat(test_targets).numpy()\n\n        # Store globally for plotting later\n        y_pred = test_preds\n        y_true = test_targets\n\n        test_rmse = math.sqrt(mean_squared_error(test_targets, test_preds))\n        test_mae = mean_absolute_error(test_targets, test_preds)\n        test_r2 = r2_score(test_targets, test_preds)\n        test_acc = regression_accuracy(test_targets, test_preds, tolerance)\n\n        metrics[\"test_rmse\"].append(test_rmse)\n        metrics[\"test_mae\"].append(test_mae)\n        metrics[\"test_r2\"].append(test_r2)\n        metrics[\"test_acc\"].append(test_acc)\n\n        print(\n            f\"\\nEpoch {epoch+1}/{num_epochs} | \"\n            f\"Train RMSE: {train_rmse:.3f}, MAE: {train_mae:.3f}, \"\n            f\"R2: {train_r2:.3f}, Acc@±{tolerance}: {train_acc*100:.2f}% | \"\n            f\"Test RMSE: {test_rmse:.3f}, MAE: {test_mae:.3f}, \"\n            f\"R2: {test_r2:.3f}, Acc@±{tolerance}: {test_acc*100:.2f}%\\n\"\n        )\n\n    return metrics\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:31:41.022796Z","iopub.execute_input":"2026-01-10T07:31:41.023585Z","iopub.status.idle":"2026-01-10T07:31:41.042402Z","shell.execute_reply.started":"2026-01-10T07:31:41.023542Z","shell.execute_reply":"2026-01-10T07:31:41.041582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# -------- LayerNorm for Conv2D (ConvNeXt style) --------\nclass LayerNorm2d(nn.Module):\n    def __init__(self, dim, eps=1e-6):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim, eps=eps)\n\n    def forward(self, x):\n        # x: (N, C, H, W) → (N, H, W, C)\n        x = x.permute(0, 2, 3, 1)\n        x = self.norm(x)\n        # back to (N, C, H, W)\n        x = x.permute(0, 3, 1, 2)\n        return x\n\n\n# -------- ConvNeXt Block --------\nclass ConvNeXtBlock(nn.Module):\n    def __init__(self, dim):\n        super().__init__()\n        self.dwconv = nn.Conv2d(dim, dim, kernel_size=7, padding=3, groups=dim)\n        self.norm = LayerNorm2d(dim)\n        self.pwconv1 = nn.Conv2d(dim, 4 * dim, kernel_size=1)\n        self.act = nn.GELU()\n        self.pwconv2 = nn.Conv2d(4 * dim, dim, kernel_size=1)\n\n    def forward(self, x):\n        residual = x\n        x = self.dwconv(x)\n        x = self.norm(x)\n        x = self.pwconv1(x)\n        x = self.act(x)\n        x = self.pwconv2(x)\n        return x + residual\n\n\n# -------- ConvNeXt Regressor --------\nclass ConvNeXtRegressor(nn.Module):\n    def __init__(self, in_channels=3, num_outputs=1):\n        super().__init__()\n\n        # Stem\n        self.stem = nn.Sequential(\n            nn.Conv2d(in_channels, 64, kernel_size=4, stride=4),\n            LayerNorm2d(64)\n        )\n\n        # Stage 1\n        self.stage1 = nn.Sequential(\n            ConvNeXtBlock(64),\n            ConvNeXtBlock(64)\n        )\n\n        # Downsample + Stage 2\n        self.down1 = nn.Sequential(\n            LayerNorm2d(64),\n            nn.Conv2d(64, 128, kernel_size=2, stride=2)\n        )\n\n        self.stage2 = nn.Sequential(\n            ConvNeXtBlock(128),\n            ConvNeXtBlock(128)\n        )\n\n        # Downsample + Stage 3\n        self.down2 = nn.Sequential(\n            LayerNorm2d(128),\n            nn.Conv2d(128, 256, kernel_size=2, stride=2)\n        )\n\n        self.stage3 = nn.Sequential(\n            ConvNeXtBlock(256),\n            ConvNeXtBlock(256)\n        )\n\n        # Head\n        self.global_pool = nn.AdaptiveAvgPool2d(1)\n        self.head = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(256, 128),\n            nn.GELU(),\n            nn.Linear(128, num_outputs)\n        )\n\n    def forward(self, x):\n        x = self.stem(x)\n        x = self.stage1(x)\n\n        x = self.down1(x)\n        x = self.stage2(x)\n\n        x = self.down2(x)\n        x = self.stage3(x)\n\n        x = self.global_pool(x)\n        x = self.head(x)\n        return x\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:31:42.790763Z","iopub.execute_input":"2026-01-10T07:31:42.791065Z","iopub.status.idle":"2026-01-10T07:31:42.802898Z","shell.execute_reply.started":"2026-01-10T07:31:42.791038Z","shell.execute_reply":"2026-01-10T07:31:42.802058Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from transformers import ViTModel, ViTImageProcessor\n\n# Load image processor (handles resizing, normalization, etc.)\nprocessor = ViTImageProcessor.from_pretrained(\n    \"google/vit-base-patch16-224\"\n)\n\n# Load pretrained ViT model\nmodel1 = ViTModel.from_pretrained(\n    \"google/vit-base-patch16-224\"\n)\n\nprint(\"ViT model downloaded and loaded successfully!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:31:45.583644Z","iopub.execute_input":"2026-01-10T07:31:45.584408Z","iopub.status.idle":"2026-01-10T07:31:45.960993Z","shell.execute_reply.started":"2026-01-10T07:31:45.584364Z","shell.execute_reply":"2026-01-10T07:31:45.96021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = ConvNeXtRegressor(in_channels=3, num_outputs=1).to(device)\nprint(f\"Model initialized on {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:31:46.078014Z","iopub.execute_input":"2026-01-10T07:31:46.078662Z","iopub.status.idle":"2026-01-10T07:31:46.116323Z","shell.execute_reply.started":"2026-01-10T07:31:46.078618Z","shell.execute_reply":"2026-01-10T07:31:46.115502Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metrics = train_and_evaluate(model1, train_loader, test_loader)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-10T07:31:46.575316Z","iopub.execute_input":"2026-01-10T07:31:46.57566Z","iopub.status.idle":"2026-01-10T07:31:48.278511Z","shell.execute_reply.started":"2026-01-10T07:31:46.575632Z","shell.execute_reply":"2026-01-10T07:31:48.277395Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport numpy as np\n\n# -------------------------------\n# Use last epoch metrics\n# -------------------------------\n# metrics is the dictionary returned by train_and_evaluate\ntrain_rmse = metrics[\"train_rmse\"][-1]\ntrain_mae  = metrics[\"train_mae\"][-1]\ntrain_r2   = metrics[\"train_r2\"][-1]\ntrain_acc  = metrics[\"train_acc\"][-1]\n\ntest_rmse = metrics[\"test_rmse\"][-1]\ntest_mae  = metrics[\"test_mae\"][-1]\ntest_r2   = metrics[\"test_r2\"][-1]\ntest_acc  = metrics[\"test_acc\"][-1]\n\nprint(\"===== Last Epoch Metrics =====\")\nprint(f\"{'Metric':<10} {'Train':>10} {'Test':>10}\")\nprint(\"-\"*32)\nprint(f\"{'RMSE':<10} {train_rmse:>10.3f} {test_rmse:>10.3f}\")\nprint(f\"{'MAE':<10} {train_mae:>10.3f} {test_mae:>10.3f}\")\nprint(f\"{'R²':<10} {train_r2:>10.3f} {test_r2:>10.3f}\")\nprint(f\"{'Acc@±10':<10} {train_acc*100:>10.2f}% {test_acc*100:>10.2f}%\")\n\n# -------------------------------\n# Plotting\n# -------------------------------\n# y_true and y_pred should still be assigned globally in train loop\ny_true = np.asarray(y_true)\ny_pred = np.asarray(y_pred)\nresiduals = y_pred - y_true\n\n# 1. Observed vs Predicted\nplt.figure(figsize=(6,6))\nplt.scatter(y_true, y_pred, alpha=0.6, color='teal',label=\"Predicted values\")\nplt.plot([y_true.min(), y_true.max()],\n         [y_true.min(), y_true.max()],\n         '--', color='red', label=\"Perfect prediction (y = x)\")\nplt.xlabel(\"Observed Biomass\")\nplt.ylabel(\"Predicted Biomass\")\nplt.title(\"Observed vs Predicted Biomass\")\nplt.grid(True)\nplt.legend()\nplt.show()\n\n# 2. Residuals vs Observed\nplt.figure(figsize=(6,4))\nplt.scatter(y_true, residuals, alpha=0.6, color='orange',label=\"Residuals\")\nplt.axhline(0, linestyle='--', color='black', label=\"Zero error line\")\nplt.xlabel(\"Observed Biomass\")\nplt.ylabel(\"Residual (Pred − Obs)\")\nplt.title(\"Residuals vs Observed Biomass\")\nplt.grid(True)\nplt.legend()\nplt.show()\n\n# 3. Error Distribution\nplt.figure(figsize=(6,4))\nplt.hist(residuals, bins=30, color='purple', alpha=0.7)\nplt.xlabel(\"Prediction Error\")\nplt.ylabel(\"Frequency\")\nplt.title(\"Error Distribution\")\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T19:54:55.314408Z","iopub.execute_input":"2026-01-08T19:54:55.314679Z","iopub.status.idle":"2026-01-08T19:54:55.722555Z","shell.execute_reply.started":"2026-01-08T19:54:55.314656Z","shell.execute_reply":"2026-01-08T19:54:55.72203Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n# -------------------------------\n# Explainability using Grad-CAM style\n# -------------------------------\ndef get_gradcam_heatmap(model, image, target_layer='stage3', device=None):\n    \"\"\"\n    Generate a Grad-CAM heatmap for a single image.\n    - model: your trained ConvNeXtRegressor\n    - image: torch tensor of shape (1, 3, H, W)\n    - target_layer: the last conv stage to visualize\n    \"\"\"\n    if device is None:\n        device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    \n    model.eval()\n    model = model.to(device)\n    image = image.to(device)\n\n    # -------------------------------\n    # Hook to capture gradients\n    # -------------------------------\n    gradients = []\n    activations = []\n\n    def backward_hook(module, grad_in, grad_out):\n        gradients.append(grad_out[0])\n\n    def forward_hook(module, input, output):\n        activations.append(output)\n\n    # Register hooks on target layer\n    layer = getattr(model, target_layer)\n    hook_forward = layer.register_forward_hook(forward_hook)\n    hook_backward = layer.register_backward_hook(backward_hook)\n\n    # Forward pass\n    output = model(image)  # Shape: (1, num_outputs)\n    pred = output.squeeze().item()\n\n    # Backward pass for Grad-CAM\n    model.zero_grad()\n    output[0].backward()  # Gradient of output w.r.t. target conv feature map\n\n    # Get activations and gradients\n    activation = activations[0].detach()         # (1, C, H, W)\n    gradient = gradients[0].detach()             # (1, C, H, W)\n\n    # Global average pooling on gradients\n    weights = gradient.mean(dim=(2, 3), keepdim=True)  # (1, C, 1, 1)\n    gradcam = (weights * activation).sum(dim=1, keepdim=True)  # (1, 1, H, W)\n    gradcam = F.relu(gradcam)\n    gradcam = F.interpolate(gradcam, size=image.shape[2:], mode='bilinear', align_corners=False)\n    gradcam = gradcam.squeeze().cpu().numpy()\n    gradcam = (gradcam - gradcam.min()) / (gradcam.max() - gradcam.min() + 1e-8)  # Normalize 0-1\n\n    # Remove hooks\n    hook_forward.remove()\n    hook_backward.remove()\n\n    return gradcam, pred\n\n# -------------------------------\n# Plot image with Grad-CAM overlay\n# -------------------------------\ndef plot_gradcam(image_tensor, heatmap, alpha=0.5):\n    \"\"\"\n    - image_tensor: (3,H,W) tensor\n    - heatmap: 2D numpy array, same HxW\n    \"\"\"\n    image = image_tensor.permute(1,2,0).cpu().numpy()\n    plt.figure(figsize=(6,6))\n    plt.imshow(image)\n    plt.imshow(heatmap, cmap='jet', alpha=alpha)\n    plt.axis('off')\n    plt.title(\"Grad-CAM Explainability\")\n    plt.show()\n\n# -------------------------------\n# USAGE EXAMPLE\n# -------------------------------\n# Pick a sample image from your dataset\nsample_image, _, sample_value = test_loader.dataset[0]  # (3,H,W)\nsample_image = sample_image.unsqueeze(0)  # Add batch dim\n\nheatmap, pred = get_gradcam_heatmap(model, sample_image)\nplot_gradcam(sample_image.squeeze(0), heatmap)\n\nprint(f\"Observed Biomass: {sample_value}, Predicted Biomass: {pred:.2f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T20:00:00.091781Z","iopub.execute_input":"2026-01-08T20:00:00.092401Z","iopub.status.idle":"2026-01-08T20:00:02.675087Z","shell.execute_reply.started":"2026-01-08T20:00:00.092371Z","shell.execute_reply":"2026-01-08T20:00:02.674338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport matplotlib.pyplot as plt\nimport random\n\ndef plot_explainability(model, dataset, num_images=5, device=None):\n    \"\"\"\n    Plots original image, Grad-CAM heatmap, and overlay for a few samples.\n    - model: trained ConvNeXtRegressor\n    - dataset: PyTorch dataset (test set)\n    - num_images: number of images to plot\n    \"\"\"\n    if device is None:\n        device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    model.eval()\n    model.to(device)\n\n    # Randomly pick indices\n    indices = random.sample(range(len(dataset)), num_images)\n\n    for idx in indices:\n        image, label, value = dataset[idx]  # image: (3,H,W)\n        image_batch = image.unsqueeze(0).to(device)  # add batch dim\n\n        # Get Grad-CAM heatmap\n        heatmap, pred = get_gradcam_heatmap(model, image_batch)\n\n        # Plot\n        fig, axes = plt.subplots(1, 3, figsize=(15,5))\n\n        # 1. Original image\n        axes[0].imshow(image.permute(1,2,0).cpu().numpy())\n        axes[0].axis('off')\n        axes[0].set_title(f\"Original\\nObserved: {value}\")\n\n        # 2. Grad-CAM heatmap\n        axes[1].imshow(heatmap, cmap='jet')\n        axes[1].axis('off')\n        axes[1].set_title(\"Grad-CAM Heatmap\")\n\n        # 3. Overlay\n        img_np = image.permute(1,2,0).cpu().numpy()\n        axes[2].imshow(img_np)\n        axes[2].imshow(heatmap, cmap='jet', alpha=0.5)\n        axes[2].axis('off')\n        axes[2].set_title(f\"Overlay\\nPredicted: {pred:.2f}\")\n\n        plt.tight_layout()\n        plt.show()\n\n# -------------------------------\n# USAGE\n# -------------------------------\nplot_explainability(model, test_loader.dataset, num_images=5)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T20:04:13.434606Z","iopub.execute_input":"2026-01-08T20:04:13.435337Z","iopub.status.idle":"2026-01-08T20:04:15.431059Z","shell.execute_reply.started":"2026-01-08T20:04:13.435308Z","shell.execute_reply":"2026-01-08T20:04:15.43039Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchinfo import summary\nsummary(model, input_size=(16, 3, 224, 448))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T20:15:09.567712Z","iopub.execute_input":"2026-01-08T20:15:09.567995Z","iopub.status.idle":"2026-01-08T20:15:09.614802Z","shell.execute_reply.started":"2026-01-08T20:15:09.56797Z","shell.execute_reply":"2026-01-08T20:15:09.614268Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Fixed measure_dataloader (reset iterator after warmup)\nimport time, numpy as np, torch\n\ndef measure_dataloader(model, loader, device=None, warmup_batches=10, n_batches=None,\n                       max_total_batches=1000, verbose=True):\n    \"\"\"\n    Fixed version: resets iterator after warmup so measurement starts from beginning.\n    Returns: numpy array of per-sample times (seconds).\n    \"\"\"\n    # normalize device (simple)\n    if isinstance(device, str):\n        device = torch.device(device)\n    elif device is None:\n        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    else:\n        try:\n            device = torch.device(str(device))\n        except Exception:\n            device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n    # Basic checks\n    if loader is None:\n        raise ValueError(\"loader is None. Provide a valid DataLoader.\")\n    try:\n        loader_len = len(loader)\n    except Exception:\n        loader_len = None\n\n    model = model.to(device)\n    model.eval()\n\n    times = []\n    total_measured_batches = 0\n    total_seen_batches = 0\n\n    it = iter(loader)\n\n    # Warmup: run some batches (wrap if loader shorter than warmup)\n    with torch.inference_mode():\n        for _ in range(warmup_batches):\n            try:\n                batch = next(it)\n            except StopIteration:\n                it = iter(loader)\n                try:\n                    batch = next(it)\n                except StopIteration:\n                    raise ValueError(\"DataLoader yielded no batches during warmup. Check dataset/transforms.\")\n            imgs = batch[0].to(device)\n            _ = model(imgs)\n        if device.type.startswith(\"cuda\"):\n            torch.cuda.synchronize()\n\n    # IMPORTANT FIX: reset iterator so timed measurements start fresh\n    it = iter(loader)\n\n    # Timed measurement loop\n    with torch.inference_mode():\n        while True:\n            if n_batches is not None and total_measured_batches >= n_batches:\n                break\n            if total_seen_batches >= max_total_batches:\n                if verbose:\n                    print(f\"[measure_dataloader] reached max_total_batches={max_total_batches}, stopping.\")\n                break\n\n            try:\n                batch = next(it)\n            except StopIteration:\n                # iterator finished\n                break\n\n            imgs = batch[0].to(device)\n            bs = imgs.size(0)\n            if bs == 0:\n                total_seen_batches += 1\n                continue\n\n            start = time.perf_counter()\n            _ = model(imgs)\n            if device.type.startswith(\"cuda\"):\n                torch.cuda.synchronize()\n            end = time.perf_counter()\n\n            batch_time = end - start\n            times.append((batch_time, bs))\n            total_measured_batches += 1\n            total_seen_batches += 1\n\n    if len(times) == 0:\n        raise ValueError(\n            \"No batches were measured. Possible causes:\\n\"\n            \" - val_loader is empty or yields zero-sized batches,\\n\"\n            \" - warmup consumed all data and iterator exhausted before measurements (fixed by reset),\\n\"\n            \" - n_batches was set to 0.\\n\"\n            \"Check your DataLoader and try setting n_batches=1 for a quick test.\"\n        )\n\n    # Expand to per-sample times\n    per_sample_times_list = [np.full(bs, (t / bs), dtype=float) for (t, bs) in times]\n    per_sample_times = np.concatenate(per_sample_times_list).astype(float)\n\n    if verbose:\n        print(f\"\\n[measure_dataloader] device={device}, batches_measured={len(times)}, total_samples_measured={per_sample_times.size}\")\n        if loader_len is not None:\n            print(f\"[measure_dataloader] reported loader_len={loader_len}\")\n        print(f\"[measure_dataloader] warmup_batches={warmup_batches}, requested_n_batches={n_batches}\")\n\n    return per_sample_times","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T20:17:03.756249Z","iopub.execute_input":"2026-01-08T20:17:03.756516Z","iopub.status.idle":"2026-01-08T20:17:03.767468Z","shell.execute_reply.started":"2026-01-08T20:17:03.756495Z","shell.execute_reply":"2026-01-08T20:17:03.766796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"per_sample_times = measure_dataloader(model, test_loader, device=device, warmup_batches=5, n_batches=None)\nprint(\"median (ms):\", np.median(per_sample_times)*1000)\nprint(\"p90 (ms):\", np.percentile(per_sample_times, 90)*1000)\nprint(\"throughput (samples/sec):\", 1.0 / per_sample_times.mean())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T20:18:22.060522Z","iopub.execute_input":"2026-01-08T20:18:22.060799Z","iopub.status.idle":"2026-01-08T20:18:45.967387Z","shell.execute_reply.started":"2026-01-08T20:18:22.060775Z","shell.execute_reply":"2026-01-08T20:18:45.96671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}