{"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,"sourceType":"competition"},{"sourceId":4533,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":3325,"modelId":986}],"dockerImageVersionId":31240,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Main Model Code (BiomassPredictor training & prediction)","metadata":{}},{"cell_type":"markdown","source":"\n\nOffline setup: Ensures PyTorch and Transformers load models without internet.\n\nImports: Loads standard libraries like PyTorch, PIL, NumPy, Pandas, and transformers for DINOv2.\n\nDevice selection: Uses GPU if available, otherwise CPU.\n\nDINOv2 backbone: Loads a pretrained DINOv2 model and unfreezes last 2 encoder blocks for fine-tuning.\n\nData preparation:\n\nExtracts image IDs from sample_id.\n\nApplies log transformation to target variables.\n\nEncodes categorical metadata (State, Species) with LabelEncoder.\n\nDataset class (GrassDataset):\n\nLoads images and metadata for training or inference.\n\nNormalizes target values during training.\n\nTransforms: Resize images, normalize, and optionally apply data augmentation.\n\nModel (BiomassPredictor):\n\nCombines DINOv2 features with metadata.\n\nPasses through a fully connected head to predict 5 biomass targets.\n\nTraining loop:\n\nUses SmoothL1Loss and AdamW optimizer with a cosine learning rate scheduler.\n\nPrediction:\n\nConverts predictions back from log-space.\n\nSaves predictions to CSV for submission.\n\nCheckpoint saving: Stores model.state_dict, mean, std, and label encoders.","metadata":{}},{"cell_type":"code","source":"# =========================\n# OFFLINE SAFETY\n# =========================\nimport os\nos.environ[\"HF_HUB_OFFLINE\"] = \"1\"\nos.environ[\"TRANSFORMERS_OFFLINE\"] = \"1\"\nos.environ[\"TORCH_HOME\"] = \"/root/.cache/torch\"\n\n# =========================\n# IMPORTS\n# =========================\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nfrom tqdm import tqdm\nfrom sklearn.preprocessing import LabelEncoder\n\n# =========================\n# DEVICE\n# =========================\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# =========================\n# LOAD DINOv2\n# =========================\nfrom transformers import AutoModel\n\nDINO_PATH = \"/kaggle/input/dinov2/pytorch/small/1\"\ndinov2 = AutoModel.from_pretrained(DINO_PATH, local_files_only=True).to(device)\n\n# 🔓 Unfreeze last 2 blocks\nfor block in dinov2.encoder.layer[-2:]:\n    for p in block.parameters():\n        p.requires_grad = True\n\n# =========================\n# DATA PREP\n# =========================\ndef prepare_data(df):\n    df[\"image_id\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n\n    # 🔥 LOG TARGET\n    df[\"target\"] = np.log1p(df[\"target\"])\n\n    le_state = LabelEncoder()\n    le_species = LabelEncoder()\n    df[\"State_encoded\"] = le_state.fit_transform(df[\"State\"])\n    df[\"Species_encoded\"] = le_species.fit_transform(df[\"Species\"])\n    return df, le_state, le_species\n\n# =========================\n# DATASET\n# =========================\nclass GrassDataset(Dataset):\n    def __init__(self, df, image_base_dir, transform, mean, std, train=True):\n        self.df = df\n        self.base = Path(image_base_dir)\n        self.transform = transform\n        self.train = train\n        self.mean = mean\n        self.std = std\n\n        self.targets = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n        self.groups = df.groupby(\"image_id\")\n        self.ids = list(self.groups.groups.keys())\n\n    def __len__(self):\n        return len(self.ids)\n\n    def __getitem__(self, idx):\n        img_id = self.ids[idx]\n        g = self.groups.get_group(img_id)\n\n        img = Image.open(self.base / g.iloc[0][\"image_path\"]).convert(\"RGB\")\n        img = self.transform(img)\n\n        meta = torch.tensor([\n            g.iloc[0][\"Pre_GSHH_NDVI\"],\n            g.iloc[0][\"Height_Ave_cm\"],\n            g.iloc[0][\"State_encoded\"],\n            g.iloc[0][\"Species_encoded\"]\n        ], dtype=torch.float32)\n\n        if not self.train:\n            return img, meta, img_id\n\n        y = []\n        for i, t in enumerate(self.targets):\n            val = g[g[\"target_name\"] == t][\"target\"].values[0]\n            y.append((val - self.mean[i]) / self.std[i])\n\n        return img, meta, torch.tensor(y, dtype=torch.float32)\n\n# =========================\n# TRANSFORMS\n# =========================\ntransform = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(0.2,0.2,0.2),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\n\n# =========================\n# MODEL\n# =========================\nclass BiomassPredictor(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = dinov2\n        self.head = nn.Sequential(\n            nn.Linear(384+4, 512),\n            nn.BatchNorm1d(512),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n            nn.GELU(),\n            nn.Dropout(0.2),\n            nn.Linear(256, 5)\n        )\n\n    def forward(self, x, meta):\n        feat = self.backbone(pixel_values=x).last_hidden_state[:,0]\n        return self.head(torch.cat([feat, meta], 1))\n\n# =========================\n# TRAIN\n# =========================\ndef train_epoch(model, loader, opt, loss_fn):\n    model.train()\n    total = 0\n    for x, m, y in tqdm(loader):\n        x,m,y = x.to(device), m.to(device), y.to(device)\n        p = model(x,m)\n        loss = loss_fn(p,y)\n        opt.zero_grad()\n        loss.backward()\n        opt.step()\n        total += loss.item()\n    return total/len(loader)\n\n# =========================\n# MAIN\n# =========================\ndef main():\n    df = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\n    df, le_state, le_species = prepare_data(df)\n\n    targets = [\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"]\n    stats = df.groupby(\"target_name\")[\"target\"].agg([\"mean\",\"std\"]).loc[targets]\n    mean, std = stats[\"mean\"].values, stats[\"std\"].values + 1e-6\n\n    ds = GrassDataset(df,\"/kaggle/input/csiro-biomass\",transform,mean,std)\n    dl = DataLoader(ds, batch_size=16, shuffle=True, num_workers=2)\n\n    model = BiomassPredictor().to(device)\n\n    opt = torch.optim.AdamW([\n        {\"params\": model.backbone.parameters(), \"lr\": 1e-5},\n        {\"params\": model.head.parameters(), \"lr\": 5e-4},\n    ], weight_decay=1e-4)\n\n    sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, 25)\n    loss_fn = nn.SmoothL1Loss()\n\n    for e in range(25):\n        l = train_epoch(model, dl, opt, loss_fn)\n        sched.step()\n        print(f\"Epoch {e+1}: {l:.4f}\")\n\n    torch.save({\n        \"model\": model.state_dict(),\n        \"mean\": mean,\n        \"std\": std,\n        \"state\": le_state,\n        \"species\": le_species\n    }, \"model.pth\")\n\n    return model, mean, std\n\n# =========================\n# PREDICT\n# =========================\ndef predict(model, mean, std):\n    df = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n    df[\"image_id\"] = df[\"sample_id\"].str.split(\"__\").str[0]\n    df[\"State_encoded\"]=0; df[\"Species_encoded\"]=0\n    df[\"Pre_GSHH_NDVI\"]=0.5; df[\"Height_Ave_cm\"]=10.0\n\n    ds = GrassDataset(df,\"/kaggle/input/csiro-biomass\",transform,mean,std,train=False)\n    model.eval()\n    out=[]\n    with torch.no_grad():\n        for x,m,i in tqdm(ds):\n            p = model(x.unsqueeze(0).to(device), m.unsqueeze(0).to(device))[0]\n            p = np.expm1(p.cpu().numpy()*std + mean)\n            for t,v in zip([\"Dry_Green_g\",\"Dry_Dead_g\",\"Dry_Clover_g\",\"GDM_g\",\"Dry_Total_g\"],p):\n                out.append({\"sample_id\":f\"{i}__{t}\",\"target\":float(v)})\n\n    pd.DataFrame(out).to_csv(\"submission.csv\", index=False)\n\n# =========================\n# RUN\n# =========================\nmodel, mean, std = main()\npredict(model, mean, std)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T06:48:35.245472Z","iopub.execute_input":"2026-01-06T06:48:35.246039Z","iopub.status.idle":"2026-01-06T06:54:33.520523Z","shell.execute_reply.started":"2026-01-06T06:48:35.246013Z","shell.execute_reply":"2026-01-06T06:54:33.519764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\n# =========================\n# LOAD MODEL CHECKPOINT\n# =========================\nmodel = BiomassPredictor().to(device)\n\n# Load full checkpoint (weights + extra objects like mean/std)\ncheckpoint = torch.load(\"model.pth\", map_location=device, weights_only=False)\n\nmodel.load_state_dict(checkpoint[\"model\"])\nmean = checkpoint[\"mean\"]\nstd = checkpoint[\"std\"]\n\nprint(\"Checkpoint loaded successfully!\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T06:57:36.973410Z","iopub.execute_input":"2026-01-06T06:57:36.973707Z","iopub.status.idle":"2026-01-06T06:57:37.071434Z","shell.execute_reply.started":"2026-01-06T06:57:36.973687Z","shell.execute_reply":"2026-01-06T06:57:37.070774Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom torchvision import transforms\n\n# =========================\n# IMAGE PATH\n# =========================\nimg_path = \"/kaggle/input/csiro-biomass/test/ID1001187975.jpg\"\nimg = Image.open(img_path).convert(\"RGB\")\n\n# =========================\n# TRANSFORM (same as training)\n# =========================\ntransform = transforms.Compose([\n    transforms.Resize((224,224)),\n    transforms.ToTensor(),\n    transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225])\n])\nimg_tensor = transform(img).unsqueeze(0).to(device)\n\n# =========================\n# DUMMY METADATA\n# =========================\nmeta = torch.tensor([[0.5, 10.0, 0, 0]], dtype=torch.float32).to(device)\n\n# =========================\n# PREDICTION\n# =========================\nmodel.eval()\nwith torch.no_grad():\n    pred = model(img_tensor, meta)[0]\n    pred = np.expm1(pred.cpu().numpy() * std + mean)\n\n# =========================\n# VISUALIZATION\n# =========================\ntargets = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\nfig, axes = plt.subplots(1, 2, figsize=(14,6))\n\n# --- Left: Image ---\naxes[0].imshow(img)\naxes[0].axis('off')\naxes[0].set_title(\"Test Image\", fontsize=14)\n\n# --- Right: Bar chart ---\nbars = axes[1].bar(targets, pred, color=['green','brown','yellow','blue','purple'])\naxes[1].set_title(\"Predicted Biomass\", fontsize=14)\naxes[1].set_ylabel(\"Biomass (g)\")\naxes[1].set_xlabel(\"Target\")\naxes[1].grid(axis='y', linestyle='--', alpha=0.6)\n\n# Annotate bars\nfor bar in bars:\n    yval = bar.get_height()\n    axes[1].text(bar.get_x() + bar.get_width()/2, yval + 0.05, f\"{yval:.2f}\", ha='center', va='bottom')\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-06T06:59:20.107938Z","iopub.execute_input":"2026-01-06T06:59:20.108671Z","iopub.status.idle":"2026-01-06T06:59:20.721162Z","shell.execute_reply.started":"2026-01-06T06:59:20.108645Z","shell.execute_reply":"2026-01-06T06:59:20.720414Z"}},"outputs":[],"execution_count":null}]}