{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# CSIRO Image2Biomass - DINOv2 Baseline\n\n**Target: Top 10 (R\u00b2 > 0.75)**\n\nApproach:\n- DINOv2 ViT-Large backbone\n- Multi-target regression (5 outputs)\n- 5-Fold CV with GroupKFold\n- TTA (4 views)\n- Heavy augmentation"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nimport gc\nimport cv2\nimport math\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nimport timm\nfrom sklearn.model_selection import GroupKFold\nfrom tqdm.auto import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"CUDA: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "class CFG:\n    # Paths\n    BASE_PATH = '/kaggle/input/csiro-biomass'\n    TRAIN_CSV = f'{BASE_PATH}/train.csv'\n    TEST_CSV = f'{BASE_PATH}/test.csv'\n    TRAIN_DIR = f'{BASE_PATH}/train'\n    TEST_DIR = f'{BASE_PATH}/test'\n    \n    # Model - usando EfficientNet que funciona seguro\n    MODEL_NAME = 'tf_efficientnetv2_m'\n    IMG_SIZE = 384\n    \n    # Training\n    DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    N_FOLDS = 5\n    FOLDS_TO_TRAIN = [0, 1, 2, 3, 4]\n    EPOCHS = 10\n    BATCH_SIZE = 8\n    GRAD_ACC = 2\n    LR = 1e-4\n    WD = 1e-2\n    WARMUP_EPOCHS = 2\n    NUM_WORKERS = 4\n    SEED = 42\n    \n    # Targets\n    TARGET_COLS = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    R2_WEIGHTS = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    \n    # TTA\n    TTA_STEPS = 4\n\ndef seed_everything(seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(CFG.SEED)\nprint(f\"Model: {CFG.MODEL_NAME}\")\nprint(f\"Image size: {CFG.IMG_SIZE}\")\nprint(f\"Device: {CFG.DEVICE}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Weighted R\u00b2 Score (competition metric)\ndef weighted_r2_score(y_true, y_pred, weights=CFG.R2_WEIGHTS):\n    r2_scores = []\n    for i in range(y_true.shape[1]):\n        yt = y_true[:, i]\n        yp = y_pred[:, i]\n        ss_res = np.sum((yt - yp) ** 2)\n        ss_tot = np.sum((yt - np.mean(yt)) ** 2)\n        r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 0.0\n        r2_scores.append(r2)\n    r2_scores = np.array(r2_scores)\n    weighted = np.sum(r2_scores * weights) / np.sum(weights)\n    return weighted, r2_scores\n\nprint(\"Metric ready\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Augmentations\ndef get_train_transforms():\n    return A.Compose([\n        A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        A.HorizontalFlip(p=0.5),\n        A.VerticalFlip(p=0.5),\n        A.RandomRotate90(p=0.5),\n        A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=0.5),\n        A.ColorJitter(brightness=0.1, contrast=0.1, saturation=0.1, hue=0.05, p=0.5),\n        A.GaussNoise(var_limit=(10.0, 50.0), p=0.3),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\ndef get_val_transforms():\n    return A.Compose([\n        A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE),\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2()\n    ])\n\ndef get_tta_transforms():\n    normalize = A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    return [\n        # Original\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), normalize, ToTensorV2()]),\n        # HFlip\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.HorizontalFlip(p=1.0), normalize, ToTensorV2()]),\n        # VFlip\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.VerticalFlip(p=1.0), normalize, ToTensorV2()]),\n        # Both flips\n        A.Compose([A.Resize(CFG.IMG_SIZE, CFG.IMG_SIZE), A.HorizontalFlip(p=1.0), A.VerticalFlip(p=1.0), normalize, ToTensorV2()]),\n    ]\n\nprint(\"Transforms ready\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Dataset\nclass BiomassDataset(Dataset):\n    def __init__(self, df, base_dir, transform=None, is_test=False):\n        self.base_dir = base_dir  # Usar BASE_PATH, no TRAIN_DIR\n        self.transform = transform\n        self.is_test = is_test\n        \n        if not is_test:\n            # Pivot to wide format\n            self.df_wide = df.pivot(index='image_path', columns='target_name', values='target').reset_index()\n            self.image_paths = self.df_wide['image_path'].values\n        else:\n            self.image_paths = df['image_path'].unique()\n    \n    def __len__(self):\n        return len(self.image_paths)\n    \n    def __getitem__(self, idx):\n        # image_path ya incluye 'train/' o 'test/', solo unir con BASE_PATH\n        img_path = os.path.join(self.base_dir, self.image_paths[idx])\n        img = cv2.imread(img_path)\n        \n        if img is None:\n            print(f\"ERROR: No se pudo leer {img_path}\")\n            # Imagen negra de fallback\n            img = np.zeros((CFG.IMG_SIZE, CFG.IMG_SIZE, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n        \n        if self.transform:\n            img = self.transform(image=img)['image']\n        \n        if self.is_test:\n            return img, self.image_paths[idx]\n        \n        targets = self.df_wide.iloc[idx][CFG.TARGET_COLS].values.astype(np.float32)\n        return img, torch.tensor(targets)\n\nprint(\"Dataset ready\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Model\nclass BiomassModel(nn.Module):\n    def __init__(self, model_name, num_targets=5, pretrained=True):\n        super().__init__()\n        self.backbone = timm.create_model(\n            model_name, \n            pretrained=pretrained, \n            num_classes=0, \n            global_pool='avg'\n        )\n        nf = self.backbone.num_features\n        \n        self.head = nn.Sequential(\n            nn.Linear(nf, nf // 2),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(nf // 2, num_targets)\n        )\n    \n    def forward(self, x):\n        features = self.backbone(x)\n        return self.head(features)\n\nprint(\"Model ready\")"}, {"cell_type": "code", "source": "# Training functions\ndef train_epoch(model, loader, optimizer, criterion, device, grad_acc=1):\n    model.train()\n    total_loss = 0\n    optimizer.zero_grad()\n    \n    for i, (images, targets) in enumerate(tqdm(loader, desc=\"Training\")):\n        images = images.to(device)\n        targets = targets.to(device)\n        \n        with torch.cuda.amp.autocast():\n            outputs = model(images)\n            loss = criterion(outputs, targets) / grad_acc\n        \n        loss.backward()\n        \n        if (i + 1) % grad_acc == 0:\n            optimizer.step()\n            optimizer.zero_grad()\n        \n        total_loss += loss.item() * grad_acc\n    \n    return total_loss / len(loader)\n\n@torch.no_grad()\ndef valid_epoch(model, loader, criterion, device):\n    model.eval()\n    total_loss = 0\n    all_preds = []\n    all_targets = []\n    \n    for images, targets in tqdm(loader, desc=\"Validating\"):\n        images = images.to(device)\n        targets = targets.to(device)\n        \n        with torch.cuda.amp.autocast():\n            outputs = model(images)\n            loss = criterion(outputs, targets)\n        \n        total_loss += loss.item()\n        all_preds.append(outputs.cpu().numpy())\n        all_targets.append(targets.cpu().numpy())\n    \n    all_preds = np.vstack(all_preds)\n    all_targets = np.vstack(all_targets)\n    \n    r2_weighted, r2_per_target = weighted_r2_score(all_targets, all_preds)\n    \n    return total_loss / len(loader), r2_weighted, r2_per_target\n\nprint(\"Training functions ready\")", "metadata": {}, "execution_count": null, "outputs": []}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Load data\nprint(\"Loading data...\")\ntrain_df = pd.read_csv(CFG.TRAIN_CSV)\ntest_df = pd.read_csv(CFG.TEST_CSV)\n\nprint(f\"Train samples: {len(train_df)}\")\nprint(f\"Test samples: {len(test_df)}\")\n\n# Get unique images and metadata for CV\ntrain_wide = train_df.pivot(index='image_path', columns='target_name', values='target').reset_index()\n\n# Merge metadata for grouping\nmeta_cols = ['image_path', 'Sampling_Date', 'State']\nmeta_df = train_df[meta_cols].drop_duplicates()\ntrain_wide = train_wide.merge(meta_df, on='image_path', how='left')\n\nprint(f\"Unique train images: {len(train_wide)}\")\nprint(f\"Targets: {CFG.TARGET_COLS}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Cross-validation training\nprint(\"=\"*60)\nprint(\"TRAINING\")\nprint(\"=\"*60)\n\ngkf = GroupKFold(n_splits=CFG.N_FOLDS)\ngroups = train_wide['Sampling_Date']\n\noof_preds = np.zeros((len(train_wide), len(CFG.TARGET_COLS)))\nfold_scores = []\n\nfor fold, (train_idx, val_idx) in enumerate(gkf.split(train_wide, groups=groups)):\n    if fold not in CFG.FOLDS_TO_TRAIN:\n        continue\n    \n    print(f\"\\n{'='*60}\")\n    print(f\"FOLD {fold + 1}/{CFG.N_FOLDS}\")\n    print(f\"Train: {len(train_idx)} | Val: {len(val_idx)}\")\n    print(\"=\"*60)\n    \n    # Prepare fold data\n    train_fold = train_df[train_df['image_path'].isin(train_wide.iloc[train_idx]['image_path'])]\n    val_fold = train_df[train_df['image_path'].isin(train_wide.iloc[val_idx]['image_path'])]\n    \n    # Usar BASE_PATH, no TRAIN_DIR\n    train_ds = BiomassDataset(train_fold, CFG.BASE_PATH, get_train_transforms())\n    val_ds = BiomassDataset(val_fold, CFG.BASE_PATH, get_val_transforms())\n    \n    train_loader = DataLoader(train_ds, batch_size=CFG.BATCH_SIZE, shuffle=True, num_workers=CFG.NUM_WORKERS)\n    val_loader = DataLoader(val_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS)\n    \n    # Model\n    model = BiomassModel(CFG.MODEL_NAME, num_targets=len(CFG.TARGET_COLS)).to(CFG.DEVICE)\n    \n    optimizer = optim.AdamW(model.parameters(), lr=CFG.LR, weight_decay=CFG.WD)\n    scheduler = CosineAnnealingLR(optimizer, T_max=CFG.EPOCHS, eta_min=1e-6)\n    criterion = nn.MSELoss()\n    \n    best_r2 = -1\n    best_model = None\n    \n    for epoch in range(CFG.EPOCHS):\n        train_loss = train_epoch(model, train_loader, optimizer, criterion, CFG.DEVICE, CFG.GRAD_ACC)\n        val_loss, val_r2, val_r2_per = valid_epoch(model, val_loader, criterion, CFG.DEVICE)\n        scheduler.step()\n        \n        print(f\"Epoch {epoch+1}: Train Loss={train_loss:.4f} | Val Loss={val_loss:.4f} | R\u00b2={val_r2:.4f}\")\n        \n        if val_r2 > best_r2:\n            best_r2 = val_r2\n            best_model = model.state_dict().copy()\n            torch.save(best_model, f'fold{fold}_best.pth')\n    \n    print(f\"\\nFold {fold + 1} Best R\u00b2: {best_r2:.4f}\")\n    fold_scores.append(best_r2)\n    \n    # Cleanup\n    del model, optimizer, scheduler, train_loader, val_loader\n    gc.collect()\n    torch.cuda.empty_cache()\n\nprint(f\"\\n{'='*60}\")\nprint(f\"CV Score: {np.mean(fold_scores):.4f} (+/- {np.std(fold_scores):.4f})\")\nprint(\"=\"*60)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Inference with TTA\nprint(\"\\nInference with TTA...\")\n\n# Usar BASE_PATH para test tambi\u00e9n\ntest_ds = BiomassDataset(test_df, CFG.BASE_PATH, get_val_transforms(), is_test=True)\ntest_loader = DataLoader(test_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS)\n\ntta_transforms = get_tta_transforms()\nall_predictions = []\n\nfor fold in CFG.FOLDS_TO_TRAIN:\n    print(f\"\\nPredicting with fold {fold + 1}...\")\n    \n    model = BiomassModel(CFG.MODEL_NAME, num_targets=len(CFG.TARGET_COLS)).to(CFG.DEVICE)\n    model.load_state_dict(torch.load(f'fold{fold}_best.pth'))\n    model.eval()\n    \n    fold_preds = []\n    \n    # TTA loop\n    for tta_idx, tta_transform in enumerate(tta_transforms[:CFG.TTA_STEPS]):\n        tta_ds = BiomassDataset(test_df, CFG.BASE_PATH, tta_transform, is_test=True)\n        tta_loader = DataLoader(tta_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS)\n        \n        preds = []\n        with torch.no_grad():\n            for images, _ in tqdm(tta_loader, desc=f\"TTA {tta_idx + 1}\"):\n                images = images.to(CFG.DEVICE)\n                with torch.cuda.amp.autocast():\n                    outputs = model(images)\n                preds.append(outputs.cpu().numpy())\n        \n        fold_preds.append(np.vstack(preds))\n    \n    # Average TTA predictions\n    fold_pred = np.mean(fold_preds, axis=0)\n    all_predictions.append(fold_pred)\n    \n    del model\n    gc.collect()\n    torch.cuda.empty_cache()\n\n# Average fold predictions\nfinal_predictions = np.mean(all_predictions, axis=0)\nprint(f\"\\nFinal predictions shape: {final_predictions.shape}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Inference with TTA\nprint(\"\\nInference with TTA...\")\n\ntest_ds = BiomassDataset(test_df, CFG.TEST_DIR, get_val_transforms(), is_test=True)\ntest_loader = DataLoader(test_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS)\n\ntta_transforms = get_tta_transforms()\nall_predictions = []\n\nfor fold in CFG.FOLDS_TO_TRAIN:\n    print(f\"\\nPredicting with fold {fold + 1}...\")\n    \n    model = BiomassModel(CFG.MODEL_NAME, num_targets=len(CFG.TARGET_COLS)).to(CFG.DEVICE)\n    model.load_state_dict(torch.load(f'fold{fold}_best.pth'))\n    model.eval()\n    \n    fold_preds = []\n    \n    # TTA loop\n    for tta_idx, tta_transform in enumerate(tta_transforms[:CFG.TTA_STEPS]):\n        tta_ds = BiomassDataset(test_df, CFG.TEST_DIR, tta_transform, is_test=True)\n        tta_loader = DataLoader(tta_ds, batch_size=CFG.BATCH_SIZE, shuffle=False, num_workers=CFG.NUM_WORKERS)\n        \n        preds = []\n        with torch.no_grad():\n            for images, _ in tqdm(tta_loader, desc=f\"TTA {tta_idx + 1}\"):\n                images = images.to(CFG.DEVICE)\n                with torch.cuda.amp.autocast():\n                    outputs = model(images)\n                preds.append(outputs.cpu().numpy())\n        \n        fold_preds.append(np.vstack(preds))\n    \n    # Average TTA predictions\n    fold_pred = np.mean(fold_preds, axis=0)\n    all_predictions.append(fold_pred)\n    \n    del model\n    gc.collect()\n    torch.cuda.empty_cache()\n\n# Average fold predictions\nfinal_predictions = np.mean(all_predictions, axis=0)\nprint(f\"\\nFinal predictions shape: {final_predictions.shape}\")"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Create submission\nprint(\"\\nCreating submission...\")\n\ntest_images = test_df['image_path'].unique()\nsubmission_rows = []\n\nfor i, img_path in enumerate(test_images):\n    for j, target_name in enumerate(CFG.TARGET_COLS):\n        submission_rows.append({\n            'sample_id': f\"{img_path}_{target_name}\",\n            'target': final_predictions[i, j]\n        })\n\nsubmission = pd.DataFrame(submission_rows)\nsubmission.to_csv('submission.csv', index=False)\n\nprint(f\"Submission shape: {submission.shape}\")\nprint(submission.head(10))"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"name": "python", "version": "3.10.0"}}, "nbformat": 4, "nbformat_minor": 4}