{"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# NDVI TRAINING — FIXED & OPTIMIZED (NO OVERFITTING)\n# ============================================================\n\nimport os, cv2, random, warnings\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import KFold\nfrom tqdm import tqdm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\nwarnings.filterwarnings(\"ignore\") # Clean up logs\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/ndvi_models\"\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n    img_size = 224\n    batch_size = 32  # INCREASED: Since we froze backbone, we can use larger batches\n    epochs = 15      # Increased epochs since convergence is slower but more stable\n    lr_head = 1e-4   # Slightly lower LR for stability\n    folds = 5\n    seed = 42\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    torch.backends.cudnn.deterministic = True\n\nos.makedirs(CFG.out_dir, exist_ok=True)\nseed_everything(CFG.seed)\n\n# ---------------- DATASET ----------------\nclass NDVIDataset(Dataset):\n    def __init__(self, df, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n        \n        # Simpler transforms to reduce noise\n        self.tfms = A.Compose([\n            A.RandomResizedCrop(\n                size=(CFG.img_size, CFG.img_size), scale=(0.9, 1.0)\n            ) if train else A.Resize(height=CFG.img_size, width=CFG.img_size),\n            A.HorizontalFlip(p=0.5) if train else A.NoOp(),\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        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((CFG.img_size, CFG.img_size, 3), dtype=np.uint8)\n\n        h, w, _ = img.shape\n        # Robust split\n        if w >= h:\n            left = img[:, :h]\n            right = img[:, w-h:]\n        else:\n            left = img\n            right = img\n\n        left = self.tfms(image=left)[\"image\"]\n        right = self.tfms(image=right)[\"image\"]\n        \n        y = torch.tensor(row.Pre_GSHH_NDVI, dtype=torch.float32)\n        return left, right, y\n\n# ---------------- MODEL (FIXED) ----------------\nclass NDVIModel(nn.Module):\n    def __init__(self):\n        super().__init__()\n        # Load DINOv2\n        self.backbone = torch.hub.load(\"facebookresearch/dinov2\", \"dinov2_vitl14\")\n        \n        # --- FIX 1: FREEZE BACKBONE ---\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        # ------------------------------\n\n        dim = self.backbone.embed_dim \n\n        # --- FIX 2: SIMPLER HEAD + HIGH DROPOUT ---\n        self.head = nn.Sequential(\n            nn.Linear(dim*2, 1024),  # Reduced from 4096\n            nn.BatchNorm1d(1024),    # Added BN for stability\n            nn.GELU(),\n            nn.Dropout(0.5),         # High dropout to prevent memorization\n            \n            nn.Linear(1024, 256),\n            nn.GELU(),\n            nn.Dropout(0.3),\n\n            nn.Linear(256, 1)\n        )\n\n    def forward(self, xl, xr):\n        with torch.no_grad(): # No gradients needed for backbone\n            fl = self.backbone(xl)\n            fr = self.backbone(xr)\n        return self.head(torch.cat([fl, fr], dim=1)).squeeze(1)\n\n# ---------------- TRAIN LOOP ----------------\ndf = pd.read_csv(CFG.train_csv)\ndf = df.drop_duplicates(\"image_path\")\ndf[\"Pre_GSHH_NDVI\"] = df[\"Pre_GSHH_NDVI\"].clip(0, 1)\n\nkf = KFold(n_splits=CFG.folds, shuffle=True, random_state=CFG.seed)\n\nfor fold, (tr, va) in enumerate(kf.split(df)):\n    print(f\"\\n================ FOLD {fold} =================\")\n\n    tr_ds = NDVIDataset(df.iloc[tr], train=True)\n    va_ds = NDVIDataset(df.iloc[va], train=False)\n\n    tr_dl = DataLoader(tr_ds, CFG.batch_size, shuffle=True, num_workers=2, pin_memory=True)\n    va_dl = DataLoader(va_ds, CFG.batch_size, shuffle=False, num_workers=2, pin_memory=True)\n\n    model = NDVIModel().to(CFG.device)\n    \n    # --- FIX 3: OPTIMIZER ONLY SEES HEAD ---\n    optimizer = torch.optim.AdamW(model.head.parameters(), lr=CFG.lr_head, weight_decay=0.01)\n    \n    # Scheduler to lower LR gently\n    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\n    loss_fn = nn.SmoothL1Loss(beta=0.05)\n    best_mae = float(\"inf\")\n\n    for epoch in range(CFG.epochs):\n        model.train()\n        tr_loss = 0\n\n        for xl, xr, y in tqdm(tr_dl, leave=False):\n            xl, xr, y = xl.to(CFG.device), xr.to(CFG.device), y.to(CFG.device)\n            \n            optimizer.zero_grad()\n            preds = model(xl, xr)\n            loss = loss_fn(preds, y)\n            loss.backward()\n            optimizer.step()\n            \n            tr_loss += loss.item()\n\n        tr_loss /= len(tr_dl)\n\n        # --- VALIDATION ---\n        model.eval()\n        va_loss = 0\n        va_mae = 0\n        \n        with torch.no_grad():\n            for xl, xr, y in va_dl:\n                xl, xr, y = xl.to(CFG.device), xr.to(CFG.device), y.to(CFG.device)\n                preds = model(xl, xr)\n                \n                va_loss += loss_fn(preds, y).item()\n                va_mae += nn.L1Loss()(preds, y).item() # Calculate Real Error\n\n        va_loss /= len(va_dl)\n        va_mae /= len(va_dl)\n        \n        # Step scheduler based on validation loss\n        scheduler.step(va_loss)\n        current_lr = optimizer.param_groups[0]['lr']\n\n        # --- FIX 4: PRINT REAL MAE ---\n        print(f\"Ep {epoch+1:02d} | Train: {tr_loss:.4f} | Val Loss: {va_loss:.4f} | MAE (Error): {va_mae:.4f} | LR: {current_lr:.1e}\")\n\n        if va_mae < best_mae:\n            best_mae = va_mae\n            torch.save(model.state_dict(), f\"{CFG.out_dir}/ndvi_fold{fold}.pth\")\n\nprint(\"\\n✅ TRAINING FINISHED\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-11T07:58:26.649902Z","iopub.execute_input":"2026-01-11T07:58:26.650231Z","iopub.status.idle":"2026-01-11T08:47:51.345159Z","shell.execute_reply.started":"2026-01-11T07:58:26.650196Z","shell.execute_reply":"2026-01-11T08:47:51.344287Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}