{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"},{"sourceId":656378,"sourceType":"modelInstanceVersion","modelInstanceId":496106,"modelId":505643},{"sourceId":657125,"sourceType":"modelInstanceVersion","modelInstanceId":496734,"modelId":505643},{"sourceId":658408,"sourceType":"modelInstanceVersion","modelInstanceId":497782,"modelId":505643},{"sourceId":662063,"sourceType":"modelInstanceVersion","modelInstanceId":500882,"modelId":505643}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nCSIRO Biomass (Kaggle) — EVA02-Large (448) Sum Pooling INFERENCE-ONLY script\n- Loads pretrained fold models from MODEL_DIR (20_2/8 checkpoints)\n- No StandardScaler - uses linear targets (raw grams)\n- Runs CV validation first, then generates test submission\n- Sum pooling aggregation over 8 tiles per image\n- 7 outputs: 5 biomass + 2 aux (Height, NDVI)\n\"\"\"\n\nimport os\nimport sys\nimport json\nimport random\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict\n\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom torchvision.transforms import InterpolationMode\n\nimport timm\nfrom timm.models import create_model\n\n# ==========================\n# Configuration\n# ==========================\nSEED = 42\nTIMM_MODEL_ID = 'eva02_large_patch14_448.mim_in22k_ft_in22k'\nIMG_SIZE = 448\nTILE_ROWS, TILE_COLS = 2, 4\nN_TILES = TILE_ROWS * TILE_COLS\nNUM_WORKERS = min(8, max(0, os.cpu_count() - 1))\nMIXED_PREC = True\nBATCH_SIZE = 1\nMAX_TILE_BATCH = 8\nDROP_PATH_RATE = 0.2\n\n# Targets order (must match training)\nTARGET_ORDER = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\nAUX_TARGETS = ['Height_Ave_cm', 'Pre_GSHH_NDVI']\nALL_TARGETS = TARGET_ORDER + AUX_TARGETS  # 7 outputs\n\n# Paths\nBASE_DIR = Path('/kaggle/input/csiro-biomass')\nHELPER_DIR = \"/kaggle/input/csiro-models/pytorch/eva02_448x448_v20_2_8/1\"\nMODEL_DIR = Path(\"/kaggle/input/csiro-models/pytorch/eva02_448x448_v20_2_8/1\")\nTILE_CACHE_DIR = Path('/kaggle/working/tile_cache')\n\nsys.path.append(HELPER_DIR)\nfrom metric import score  # competition-provided metric\n\n# Input files\nTRAIN_CSV = BASE_DIR / 'train.csv'\nTEST_CSV = BASE_DIR / 'test.csv'\nCV_JSON = Path(HELPER_DIR) / 'cv_splits.json'\n\n# Output\nSUBMISSION_CSV = Path('submission.csv')\n\n# ==========================\n# Utils\n# ==========================\n\ndef seed_everything(seed: int = 42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\n\ndef resolve_path(rel_path: str) -> Path:\n    p = BASE_DIR / rel_path\n    if p.exists():\n        return p\n    if rel_path.startswith('images/'):\n        alt = BASE_DIR / rel_path.replace('images/', 'train/', 1)\n        if alt.exists():\n            return alt\n    return p\n\n\ndef tile_image_to_cache(src_path: Path, cache_dir: Path) -> List[Path]:\n    \"\"\"Split image into 2x4 grid of centered square tiles.\"\"\"\n    TILE_CACHE_DIR.mkdir(parents=True, exist_ok=True)\n\n    img = Image.open(src_path).convert('RGB')\n    W, H = img.size\n    tile_size = min(W // TILE_COLS, H // TILE_ROWS)\n    new_W = tile_size * TILE_COLS\n    new_H = tile_size * TILE_ROWS\n    left = (W - new_W) // 2\n    top = (H - new_H) // 2\n    img = img.crop((left, top, left + new_W, top + new_H))\n\n    tiles = []\n    basename = src_path.stem\n    out_subdir = cache_dir / basename\n    out_subdir.mkdir(parents=True, exist_ok=True)\n\n    idx = 0\n    for r in range(TILE_ROWS):\n        for c in range(TILE_COLS):\n            x0 = c * tile_size\n            y0 = r * tile_size\n            tile = img.crop((x0, y0, x0 + tile_size, y0 + tile_size))\n            tile_path = out_subdir / f'{basename}_tile{idx}.png'\n            if not tile_path.exists():\n                tile.save(tile_path)\n            tiles.append(tile_path)\n            idx += 1\n    return tiles\n\n\ndef create_transforms(mean, std):\n    \"\"\"Validation transforms only (no augmentation).\"\"\"\n    return transforms.Compose([\n        transforms.Resize((IMG_SIZE, IMG_SIZE), interpolation=InterpolationMode.BICUBIC),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=mean, std=std),\n    ])\n\n\n# ==========================\n# Data preparation\n# ==========================\n\ndef load_and_pivot_train(train_csv: Path) -> pd.DataFrame:\n    \"\"\"Load and pivot train data to wide format.\"\"\"\n    df = pd.read_csv(train_csv)\n\n    # Pivot biomass targets\n    df_filtered = df[df['target_name'].isin(TARGET_ORDER)].copy()\n    bio_wide = df_filtered.pivot_table(index='image_path', columns='target_name', values='target', aggfunc='mean')\n    bio_wide = bio_wide.reindex(columns=TARGET_ORDER)\n\n    # Aux targets (one row per image)\n    aux_df = df.groupby('image_path')[AUX_TARGETS].first()\n\n    # Join\n    wide = bio_wide.join(aux_df, how='left')\n\n    # Fill missing aux with median (shouldn't have any, but defensive)\n    for col in AUX_TARGETS:\n        if wide[col].isna().any():\n            med = float(wide[col].median())\n            wide[col] = wide[col].fillna(med)\n\n    wide = wide.reset_index()\n    return wide\n\n\nclass TiledDataset(Dataset):\n    \"\"\"Dataset that loads 8 tiles per image from cache.\"\"\"\n    def __init__(self, image_paths: List[str], targets: np.ndarray, transform, cache_dir: Path):\n        self.image_paths = image_paths\n        self.targets = targets  # [N, 7] linear targets (5 biomass + 2 aux)\n        self.transform = transform\n        self.cache_dir = cache_dir\n\n    def __len__(self):\n        return len(self.image_paths)\n\n    def _load_tiles(self, img_rel_path: str) -> List[Image.Image]:\n        src = resolve_path(img_rel_path)\n        base = src.stem\n        subdir = self.cache_dir / base\n        tiles = []\n        for i in range(N_TILES):\n            tile_path = subdir / f'{base}_tile{i}.png'\n            if not tile_path.exists():\n                tile_image_to_cache(src, self.cache_dir)\n            tiles.append(Image.open(tile_path).convert('RGB'))\n        return tiles\n\n    def __getitem__(self, idx):\n        img_rel = self.image_paths[idx]\n        pil_tiles = self._load_tiles(img_rel)\n        tiles = [self.transform(t) for t in pil_tiles]\n        x = torch.stack(tiles, dim=0)  # [8, 3, 448, 448]\n\n        if self.targets is None:\n            y = torch.zeros(len(ALL_TARGETS), dtype=torch.float32)\n        else:\n            y = torch.tensor(self.targets[idx], dtype=torch.float32)\n\n        return x, y, img_rel\n\n\n# ==========================\n# Model\n# ==========================\n\nclass SumPooling(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        # x: [B, T, D]\n        return x.sum(dim=1)  # [B, D]\n\n\nclass SumPoolEVARegressor(nn.Module):\n    def __init__(self, timm_model_id: str, pretrained: bool = True, out_dim: int = 7,\n                 max_tile_batch: int = 8, drop_path_rate: float = 0.0):\n        super().__init__()\n        self.backbone = create_model(timm_model_id, pretrained=pretrained, num_classes=0,\n                                    drop_path_rate=drop_path_rate)\n        embed_dim = getattr(self.backbone, 'num_features', None)\n        if embed_dim is None:\n            raise AttributeError('Backbone missing num_features')\n        self.pool = SumPooling()\n        self.head = nn.Linear(embed_dim, out_dim)\n        self.max_tile_batch = max_tile_batch\n\n    def forward(self, tiles: torch.Tensor) -> torch.Tensor:\n        if tiles.dim() == 4:\n            tiles = tiles.unsqueeze(0)\n        B, T, C, H, W = tiles.shape\n        tiles = tiles.view(B * T, C, H, W)\n\n        # Process tiles in batches to avoid OOM\n        feats = []\n        for start in range(0, tiles.size(0), self.max_tile_batch):\n            end = min(start + self.max_tile_batch, tiles.size(0))\n            f = self.backbone(tiles[start:end])  # [chunk, D]\n            feats.append(f)\n        feats = torch.cat(feats, dim=0).view(B, T, -1)  # [B, T, D]\n        pooled = self.pool(feats)                       # [B, D]\n        out = self.head(pooled)                         # [B, 7]\n        return out\n\n\n# ==========================\n# Evaluation (CV validation)\n# ==========================\n\ndef evaluate(model: nn.Module, loader: DataLoader, device: torch.device) -> Tuple[float, Dict[str, np.ndarray]]:\n    model.eval()\n    y_true_list, y_pred_list, paths_list = [], [], []\n\n    with torch.no_grad():\n        for tiles, y_true, rel_paths in loader:\n            tiles = tiles.to(device, non_blocking=True)\n\n            with torch.cuda.amp.autocast(enabled=MIXED_PREC):\n                preds = model(tiles)  # [B, 7]\n\n            # Clip biomass predictions to >=0 (first 5 outputs)\n            preds_np = preds.cpu().numpy()\n            preds_np[:, :5] = np.clip(preds_np[:, :5], 0.0, None)\n\n            y_true_list.append(y_true.numpy())\n            y_pred_list.append(preds_np)\n            paths_list.extend(rel_paths)\n\n    y_true = np.concatenate(y_true_list, axis=0)\n    y_pred = np.concatenate(y_pred_list, axis=0)\n\n    # Score only biomass (first 5 outputs)\n    val_metric = float(score(torch.tensor(y_true[:, :5]), torch.tensor(y_pred[:, :5])))\n\n    return val_metric, {\n        'image_paths': paths_list,\n        'y_true': y_true,\n        'y_pred': y_pred,\n    }\n\n\n# ==========================\n# Main\n# ==========================\n\ndef main():\n    seed_everything(SEED)\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    print(f\"Using device: {device}\")\n    print(f\"Loading models from: {MODEL_DIR}\")\n\n    # Normalization constants for EVA02 (CLIP-style)\n    mean = (0.48145466, 0.4578275, 0.40821073)\n    std = (0.26862954, 0.26130258, 0.27577711)\n    print(f\"Using normalization: mean={mean}, std={std}\")\n\n    # Load data\n    df_wide = load_and_pivot_train(TRAIN_CSV)\n    print(f\"Loaded train wide dataframe: {df_wide.shape[0]} images\")\n\n    # Read CV splits\n    with open(CV_JSON, 'r') as f:\n        cv_info = json.load(f)\n    folds = cv_info['folds']\n    n_folds = len(folds)\n    print(f\"Number of CV folds: {n_folds}\")\n\n    # Transforms\n    val_tfms = create_transforms(mean, std)\n\n    # Cache tiles for train images\n    train_image_paths = df_wide['image_path'].tolist()\n    print(f\"Caching tiles for {len(train_image_paths)} training images...\")\n    for img_path in train_image_paths:\n        src = resolve_path(img_path)\n        tile_image_to_cache(src, TILE_CACHE_DIR)\n\n    # ==========================\n    # Phase 1: Run CV validation for each fold\n    # ==========================\n    print(\"\\n\" + \"=\"*50)\n    print(\"Phase 1: Running CV validation for each fold\")\n    print(\"=\"*50)\n\n    cv_scores = []\n    fold_models = []\n\n    for fold_idx in range(n_folds):\n        fold = folds[fold_idx]\n        fold_val_images = [p for p in fold['val_images'] if p in set(df_wide['image_path'])]\n\n        print(f\"\\nFold {fold_idx}: val images = {len(fold_val_images)}\")\n\n        # Load model for this fold\n        model_path = MODEL_DIR / f\"model_fold{fold_idx}.pt\"\n        if not model_path.exists():\n            print(f\"  WARNING: Model not found at {model_path}, skipping fold {fold_idx}\")\n            continue\n\n        ckpt = torch.load(model_path, map_location='cpu')\n\n        model = SumPoolEVARegressor(TIMM_MODEL_ID, pretrained=False, out_dim=len(ALL_TARGETS),\n                                    max_tile_batch=MAX_TILE_BATCH, drop_path_rate=DROP_PATH_RATE)\n        model.load_state_dict(ckpt['model_state'])\n        model.to(device)\n        model.eval()\n        print(f\"  Loaded model from {model_path}\")\n\n        fold_models.append((fold_idx, model))\n\n        # Get targets for validation fold\n        val_df = df_wide[df_wide['image_path'].isin(fold_val_images)]\n        val_targets = val_df[ALL_TARGETS].values.astype(np.float32)\n\n        # Create validation dataset and dataloader\n        ds_val = TiledDataset(fold_val_images, val_targets, val_tfms, TILE_CACHE_DIR)\n        dl_val = DataLoader(ds_val, batch_size=BATCH_SIZE, shuffle=False,\n                           num_workers=NUM_WORKERS, pin_memory=True)\n\n        print(f\"  Val images = {len(ds_val)}\")\n\n        # Run validation\n        val_metric, pack = evaluate(model, dl_val, device)\n        cv_scores.append(val_metric)\n        print(f\"  Fold {fold_idx} CV score: {val_metric:.5f}\")\n\n    # CV summary\n    if len(cv_scores) > 0:\n        cv_mean = float(np.mean(cv_scores))\n        cv_std = float(np.std(cv_scores))\n        print(f\"\\nCV scores per fold: {cv_scores}\")\n        print(f\"CV mean = {cv_mean:.5f}, std = {cv_std:.5f}\")\n    else:\n        print(\"WARNING: No CV scores computed (no models found)\")\n\n    # ==========================\n    # Phase 2: Test inference and submission\n    # ==========================\n    print(\"\\n\" + \"=\"*50)\n    print(\"Phase 2: Running test inference\")\n    print(\"=\"*50)\n\n    test_df = pd.read_csv(TEST_CSV)\n    test_images = test_df['image_path'].unique().tolist()\n    print(f\"Test images: {len(test_images)}\")\n\n    # Cache test tiles\n    print(f\"Caching tiles for {len(test_images)} test images...\")\n    for img_path in test_images:\n        src = resolve_path(img_path)\n        tile_image_to_cache(src, TILE_CACHE_DIR)\n\n    if len(fold_models) == 0:\n        print(\"ERROR: No models loaded, cannot generate submission\")\n        return\n\n    # Predict test images with each fold model\n    def predict_images_with_model(model: nn.Module, image_paths: List[str]) -> Dict[str, np.ndarray]:\n        model.eval()\n        out = {}\n        with torch.no_grad():\n            for rp in image_paths:\n                src = resolve_path(rp)\n                base = src.stem\n                subdir = TILE_CACHE_DIR / base\n\n                tiles_pil = []\n                for i in range(N_TILES):\n                    tile_path = subdir / f'{base}_tile{i}.png'\n                    tiles_pil.append(Image.open(tile_path).convert('RGB'))\n\n                tiles = torch.stack([val_tfms(t) for t in tiles_pil], dim=0).unsqueeze(0).to(device)\n\n                with torch.cuda.amp.autocast(enabled=MIXED_PREC):\n                    preds = model(tiles)  # [1, 7]\n\n                # Clip biomass (first 5) to >=0\n                preds_np = preds.cpu().numpy()[0]\n                biomass = np.clip(preds_np[:5], 0.0, None).astype(np.float32)\n                out[rp] = biomass\n        return out\n\n    # Aggregate predictions over folds\n    preds_by_image = {rp: [] for rp in test_images}\n\n    for fold_idx, model in fold_models:\n        print(f\"Running inference with fold {fold_idx} model...\")\n        fold_preds = predict_images_with_model(model, test_images)\n        for rp in test_images:\n            preds_by_image[rp].append(fold_preds[rp])\n\n    # Average per image\n    final_preds = {}\n    for rp in test_images:\n        arr = np.stack(preds_by_image[rp], axis=0)\n        avg = arr.mean(axis=0)\n        avg = np.clip(avg, 0.0, None)\n        final_preds[rp] = avg\n\n    # Build submission\n    tgt_to_idx = {t: i for i, t in enumerate(TARGET_ORDER)}\n    sub_rows = []\n    for _, row in test_df.iterrows():\n        rp = row['image_path']\n        tname = row['target_name']\n        pred_val = float(final_preds[rp][tgt_to_idx[tname]])\n        sub_rows.append({'sample_id': row['sample_id'], 'target': pred_val})\n\n    sub_df = pd.DataFrame(sub_rows)\n    sub_df.to_csv(SUBMISSION_CSV, index=False)\n    print(f\"\\nSubmission saved to: {SUBMISSION_CSV}\")\n\n    # Log submission distribution summary\n    print(\"\\nSubmission distribution summary:\")\n    for tname in TARGET_ORDER:\n        vals = [final_preds[rp][tgt_to_idx[tname]] for rp in test_images]\n        arr = np.array(vals, dtype=np.float64)\n        print(f\"  {tname}: min={arr.min():.2f}, max={arr.max():.2f}, mean={arr.mean():.2f}, std={arr.std():.2f}\")\n\n    print(\"\\nInference complete!\")\n\n\nif __name__ == '__main__':\n    main()\n","metadata":{"_uuid":"ec35d824-9729-42ca-8304-025bbd9006f4","_cell_guid":"caa171cc-f654-4f2a-be82-243d9a4ca0c4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null}]}