{"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":"# 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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n#  ROBUST HEIGHT MODEL (SIGMOID SCALING + CLEAN DATA)\n# ============================================================\nimport os\nimport cv2\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import mean_absolute_error, r2_score\nfrom tqdm import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nwarnings.filterwarnings(\"ignore\")\n\n# ---------------- CONFIG ----------------\nclass CFG:\n    data_root = \"/kaggle/input/csiro-biomass\"\n    train_csv = f\"{data_root}/train.csv\"\n    out_dir = \"/kaggle/working/height_models\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    img_size = 224\n    batch_size = 8       # Small batch for DINO Large\n    accum_iter = 4       # Effective batch 32\n    \n    epochs = 15\n    folds = 5\n    seed = 42\n    \n    # CONSTRAINTS\n    max_height = 50.0    # We assume grass is never taller than 50cm\n    \n    # OPTIMIZER\n    lr_backbone = 1e-6\n    lr_head = 5e-4\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n\nos.makedirs(CFG.out_dir, exist_ok=True)\nseed_everything(CFG.seed)\n\n# ---------------- 1. CLEAN DATA ----------------\nprint(\"Loading and Cleaning Data...\")\ndf = pd.read_csv(CFG.train_csv)\n\n# DROP OUTLIERS: Heights should be positive and reasonable\ninitial_len = len(df)\ndf = df[df['Height_Ave_cm'] > 0] \ndf = df[df['Height_Ave_cm'] < 60] # Remove extreme outliers if any\nprint(f\"Dropped {initial_len - len(df)} rows of bad data.\")\nprint(f\"New Mean Height: {df['Height_Ave_cm'].mean():.2f} cm\")\n\ndf = df.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n\n# ---------------- 2. DATASET (Simple Scaling) ----------------\nclass TripleCropDataset(Dataset):\n    def __init__(self, df, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n        \n        self.aug = A.Compose([\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n        self.val_aug = A.Compose([\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = os.path.join(CFG.data_root, row.image_path)\n        \n        # Load Image\n        try:\n            img = cv2.imread(path)\n            if img is None: raise ValueError\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        except:\n            img = np.zeros((224, 224, 3), dtype=np.uint8)\n\n        h, w, c = img.shape\n        crop_w = w // 2\n        crops_raw = [\n            img[:, 0:crop_w, :],                # Left\n            img[:, w//4 : w//4 + crop_w, :],    # Center\n            img[:, w-crop_w:, :]                # Right\n        ]\n        \n        final_crops = []\n        for c_img in crops_raw:\n            c_img = cv2.resize(c_img, (CFG.img_size, CFG.img_size), interpolation=cv2.INTER_AREA)\n            if self.train:\n                c_img = self.aug(image=c_img)[\"image\"]\n            else:\n                c_img = self.val_aug(image=c_img)[\"image\"]\n            final_crops.append(c_img)\n            \n        img_stack = torch.stack(final_crops)\n        \n        # --- SIMPLE SCALING ---\n        # Normalize 0-50cm to 0.0-1.0\n        # If height is 15cm, label is 0.3\n        raw_h = row['Height_Ave_cm']\n        label = float(raw_h) / CFG.max_height \n        \n        return img_stack, torch.tensor(label, dtype=torch.float32)\n\n# ---------------- 3. MODEL (Sigmoid Bound) ----------------\nclass BoundedHeightModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = torch.hub.load(\"facebookresearch/dinov2\", \"dinov2_vitl14\")\n        \n        # Freeze most, unfreeze last blocks\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        for param in self.backbone.blocks[-2:].parameters():\n            param.requires_grad = True\n        for param in self.backbone.norm.parameters():\n            param.requires_grad = True\n            \n        embed_dim = 1024\n        \n        # Attention\n        self.attn = nn.Sequential(\n            nn.Linear(embed_dim, 256),\n            nn.Tanh(),\n            nn.Linear(256, 1)\n        )\n        \n        # Simplified Robust Head\n        self.head = nn.Sequential(\n            nn.Linear(embed_dim, 512),\n            nn.BatchNorm1d(512),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 1) \n            # NO ACTIVATION HERE, we do it in Forward\n        )\n\n    def forward(self, x):\n        b, n, c, h, w = x.shape\n        x = x.view(b*n, c, h, w)\n        \n        feat = self.backbone(x).view(b, n, -1)\n        \n        # Attention\n        weights = F.softmax(self.attn(feat), dim=1)\n        ctx = torch.sum(feat * weights, dim=1)\n        \n        # Prediction\n        out = self.head(ctx)\n        \n        # --- SIGMOID BOUNDING ---\n        # Forces output to be (0, 1) then scales to (0, 50)\n        return torch.sigmoid(out) \n\n# ---------------- 4. TRAIN LOOP ----------------\nkf = KFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)\n\nfor fold, (tr_idx, va_idx) in enumerate(kf.split(df)):\n    print(f\"\\n=== FOLD {fold} ===\")\n    \n    tr_ds = TripleCropDataset(df.iloc[tr_idx], train=True)\n    va_ds = TripleCropDataset(df.iloc[va_idx], train=False)\n    \n    tr_dl = DataLoader(tr_ds, CFG.batch_size, shuffle=True, num_workers=2, drop_last=True)\n    va_dl = DataLoader(va_ds, CFG.batch_size, shuffle=False, num_workers=2)\n    \n    model = BoundedHeightModel().to(CFG.device)\n    \n    optimizer = torch.optim.AdamW([\n        {'params': model.backbone.parameters(), 'lr': CFG.lr_backbone},\n        {'params': model.head.parameters(), 'lr': CFG.lr_head}\n    ])\n    \n    scheduler = torch.optim.lr_scheduler.OneCycleLR(\n        optimizer, max_lr=[CFG.lr_backbone*5, CFG.lr_head], \n        total_steps=CFG.epochs*len(tr_dl), pct_start=0.2\n    )\n    \n    loss_fn = nn.L1Loss()\n    \n    best_mae = float('inf')\n    \n    for epoch in range(CFG.epochs):\n        model.train()\n        tr_loss = 0\n        \n        # TRAIN\n        for i, (imgs, labels) in enumerate(tqdm(tr_dl, leave=False, desc=f\"Ep {epoch+1}\")):\n            imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n            \n            # Model outputs 0-1\n            preds = model(imgs).squeeze(1)\n            \n            loss = loss_fn(preds, labels)\n            loss.backward()\n            \n            if (i+1) % CFG.accum_iter == 0:\n                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n                optimizer.step()\n                scheduler.step()\n                optimizer.zero_grad()\n                \n            tr_loss += loss.item()\n            \n        # VALIDATE\n        model.eval()\n        va_diff = []\n        \n        with torch.no_grad():\n            for imgs, labels in va_dl:\n                imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n                \n                # Predict (0-1)\n                preds = model(imgs).squeeze(1)\n                \n                # Convert back to CM for Metrics\n                preds_cm = preds.cpu().numpy() * CFG.max_height\n                targets_cm = labels.cpu().numpy() * CFG.max_height\n                \n                va_diff.extend(np.abs(preds_cm - targets_cm))\n        \n        mae = np.mean(va_diff)\n        print(f\"Ep {epoch+1} | Loss: {tr_loss/len(tr_dl):.4f} | Val MAE: {mae:.4f} cm\")\n        \n        if mae < best_mae:\n            best_mae = mae\n            torch.save(model.state_dict(), f\"{CFG.out_dir}/best_height_fold{fold}.pth\")\n            \n    break # Train 1 fold first to test","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T16:04:22.189100Z","iopub.execute_input":"2026-01-12T16:04:22.189727Z","iopub.status.idle":"2026-01-12T16:21:20.675291Z","shell.execute_reply.started":"2026-01-12T16:04:22.189689Z","shell.execute_reply":"2026-01-12T16:21:20.674282Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n#  ROBUST HEIGHT MODEL (TRAINING FOLDS 1, 2, 3, 4)\n# ============================================================\nimport os\nimport cv2\nimport random\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import KFold\nfrom sklearn.metrics import mean_absolute_error\nfrom tqdm import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nwarnings.filterwarnings(\"ignore\")\n\n# ---------------- CONFIG ----------------\nclass CFG:\n    data_root = \"/kaggle/input/csiro-biomass\"\n    train_csv = f\"{data_root}/train.csv\"\n    out_dir = \"/kaggle/working/height_models\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    img_size = 224\n    batch_size = 8        \n    accum_iter = 4        \n    \n    epochs = 15           \n    folds = 5\n    seed = 42\n    \n    max_height = 50.0     \n    lr_backbone = 1e-6    \n    lr_head = 5e-4        \n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n\nos.makedirs(CFG.out_dir, exist_ok=True)\nseed_everything(CFG.seed)\n\n# ---------------- PREPARE DATA ----------------\ndf = pd.read_csv(CFG.train_csv)\ndf = df[df['Height_Ave_cm'] > 0] \ndf = df[df['Height_Ave_cm'] < 60] \ndf = df.drop_duplicates(subset=['image_path']).reset_index(drop=True)\n\n# ---------------- DATASET ----------------\nclass TripleCropDataset(Dataset):\n    def __init__(self, df, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n        self.aug = A.Compose([\n            A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5),\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n        self.val_aug = A.Compose([\n            A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n            ToTensorV2()\n        ])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        path = os.path.join(CFG.data_root, row.image_path)\n        try:\n            img = cv2.imread(path)\n            if img is None: raise ValueError\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        except:\n            img = np.zeros((224, 224, 3), dtype=np.uint8)\n\n        h, w, c = img.shape\n        crop_w = w // 2\n        # Create 3 distinct crops (Left, Center, Right)\n        crops_raw = [img[:, 0:crop_w, :], img[:, w//4 : w//4 + crop_w, :], img[:, w-crop_w:, :]]\n        \n        final_crops = []\n        for c_img in crops_raw:\n            c_img = cv2.resize(c_img, (CFG.img_size, CFG.img_size), interpolation=cv2.INTER_AREA)\n            if self.train: c_img = self.aug(image=c_img)[\"image\"]\n            else: c_img = self.val_aug(image=c_img)[\"image\"]\n            final_crops.append(c_img)\n            \n        return torch.stack(final_crops), torch.tensor(float(row['Height_Ave_cm']) / CFG.max_height, dtype=torch.float32)\n\n# ---------------- MODEL ----------------\nclass BoundedHeightModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.backbone = torch.hub.load(\"facebookresearch/dinov2\", \"dinov2_vitl14\")\n        for param in self.backbone.parameters(): param.requires_grad = False\n        for param in self.backbone.blocks[-2:].parameters(): param.requires_grad = True\n        for param in self.backbone.norm.parameters(): param.requires_grad = True\n            \n        self.attn = nn.Sequential(nn.Linear(1024, 256), nn.Tanh(), nn.Linear(256, 1))\n        self.head = nn.Sequential(\n            nn.Linear(1024, 512), nn.BatchNorm1d(512), nn.GELU(), nn.Dropout(0.3), nn.Linear(512, 1)\n        )\n\n    def forward(self, x):\n        b, n, c, h, w = x.shape\n        x = x.view(b*n, c, h, w)\n        feat = self.backbone(x).view(b, n, -1)\n        # Attention pooling over the 3 crops\n        weights = F.softmax(self.attn(feat), dim=1)\n        return torch.sigmoid(self.head(torch.sum(feat * weights, dim=1)))\n\n# ---------------- LOOP (SKIPPING FOLD 0) ----------------\nkf = KFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)\n\nfor fold, (tr_idx, va_idx) in enumerate(kf.split(df)):\n    \n    # --- SKIP FOLD 0 (Since you already have it) ---\n    if fold == 0:\n        print(f\"Skipping Fold {fold} (Already trained)...\")\n        continue\n\n    print(f\"\\n=== FOLD {fold} ===\")\n    \n    tr_ds = TripleCropDataset(df.iloc[tr_idx], train=True)\n    va_ds = TripleCropDataset(df.iloc[va_idx], train=False)\n    \n    tr_dl = DataLoader(tr_ds, CFG.batch_size, shuffle=True, num_workers=2, drop_last=True)\n    va_dl = DataLoader(va_ds, CFG.batch_size, shuffle=False, num_workers=2)\n    \n    model = BoundedHeightModel().to(CFG.device)\n    \n    optimizer = torch.optim.AdamW([\n        {'params': model.backbone.parameters(), 'lr': CFG.lr_backbone},\n        {'params': model.head.parameters(), 'lr': CFG.lr_head}\n    ])\n    \n    # ---------------------------------------------------------\n    # FIX: Removed 'verbose=True' to fix TypeError\n    # ---------------------------------------------------------\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.5, patience=2\n    )\n    \n    loss_fn = nn.L1Loss()\n    best_mae = float('inf')\n    \n    for epoch in range(CFG.epochs):\n        model.train()\n        tr_loss = 0\n        \n        loop = tqdm(tr_dl, leave=False, desc=f\"Ep {epoch+1}\")\n        for i, (imgs, labels) in enumerate(loop):\n            imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n            \n            loss = loss_fn(model(imgs).squeeze(1), labels) / CFG.accum_iter\n            loss.backward()\n            \n            if (i+1) % CFG.accum_iter == 0:\n                torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)\n                optimizer.step()\n                optimizer.zero_grad()\n                \n            tr_loss += loss.item() * CFG.accum_iter\n            \n        # VALIDATE\n        model.eval()\n        va_diff = []\n        with torch.no_grad():\n            for imgs, labels in va_dl:\n                imgs, labels = imgs.to(CFG.device), labels.to(CFG.device)\n                preds_cm = model(imgs).squeeze(1).cpu().numpy() * CFG.max_height\n                targets_cm = labels.cpu().numpy() * CFG.max_height\n                va_diff.extend(np.abs(preds_cm - targets_cm))\n        \n        mae = np.mean(va_diff)\n        scheduler.step(mae)\n        \n        # We manually print the LR now since verbose is gone\n        current_lr = optimizer.param_groups[1]['lr']\n        print(f\"Ep {epoch+1:02d} | Loss: {tr_loss/len(tr_dl):.4f} | Val MAE: {mae:.4f} cm | LR: {current_lr:.2e}\")\n        \n        if mae < best_mae:\n            best_mae = mae\n            torch.save(model.state_dict(), f\"{CFG.out_dir}/best_height_fold{fold}.pth\")\n            print(f\"  >>> New Best Saved!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-12T16:35:36.191518Z","iopub.execute_input":"2026-01-12T16:35:36.192202Z","iopub.status.idle":"2026-01-12T17:43:40.554909Z","shell.execute_reply.started":"2026-01-12T16:35:36.192155Z","shell.execute_reply":"2026-01-12T17:43:40.553879Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}