{
  "id": 670759,
  "title": "123rd (public 6th) Solution - Tile-based ViT with 2D Pos-Emb & MIL pooling",
  "url": "/competitions/csiro-biomass/writeups/129th-public-6th-solution-tile-based-vit-with",
  "author_name": "",
  "post_date": "2026-01-29T18:24:58.640Z",
  "votes": 17,
  "comment_count": 10,
  "views": 0,
  "content": "<h1>TL;DR</h1>\n<p>Our approach reconstructs the input image into a 32-tile grid and learns the global representation using ViT + 2D positional embeddings + Multi-head MIL attention pooling. Instead of predicting all 5 targets directly, we predict the 3 main targets (<code>DG</code>, <code>GDM</code>, <code>DT</code>) and physically reconstruct the remaining two (<code>DD</code>, <code>DC</code>) to align with the metric. We also utilized LUPI (Learning Using Privileged Information) via a meta-teacher to distill knowledge from train-only metadata into the vision model, enabling image-only inference.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F13143896%2F3faf64ba4b9134cb37becc3432355853%2Fimage%20(2).png?generation=1769698780181835&amp;alt=media\" alt=\"Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling\">\n<strong>Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling</strong></p>\n<h2>1. Data preprocessing</h2>\n<h3>Features</h3>\n<ul>\n<li><strong>Meta</strong>: <code>Pre_GSHH_NDVI</code>, <code>Height_Ave_cm</code>, <code>Sampling_Date</code>, <code>State</code>, <code>Species</code></li>\n<li><strong>Derived</strong><ul>\n<li><code>Month</code>: extracted from <code>Sampling_Date</code></li>\n<li><code>State_encoded</code>, <code>Species_encoded</code>: label encoding</li></ul></li>\n</ul>\n<h3>Quality meta (train-time meta feature)</h3>\n<p>(Ref. <a href=\"https://www.kaggle.com/competitions/csiro-biomass/discussion/667537\" target=\"_blank\">https://www.kaggle.com/competitions/csiro-biomass/discussion/667537</a>)</p>\n<ul>\n<li><code>q_sharpness</code>: Laplacian variance (sharpness)</li>\n<li><code>q_noise</code>: residual mean after Gaussian blur (noise proxy)</li>\n<li><code>q_device_cluster</code>: <code>StandardScaler + KMeans(n_clusters=2)</code></li>\n</ul>\n<h3>32-tile input construction (4×8 global grid)</h3>\n<ul>\n<li>Original image: <strong>2000×1000</strong></li>\n<li>Crops (each 1000×1000)<ul>\n<li>Left: x 0–1000</li>\n<li>Middle: x 500–1500</li>\n<li>Right: x 1000–2000</li></ul></li>\n<li>Build 32 tiles of 250×250 on a global 4×8 grid:<ul>\n<li>global col 0..1: from <strong>Left</strong> (local col 0..1) → 8 tiles</li>\n<li>global col 2..5: from <strong>Middle</strong> (local col 0..3) → 16 tiles</li>\n<li>global col 6..7: from <strong>Right</strong> (local col 2..3) → 8 tiles\n→ total 32 tiles (row-major order)</li></ul></li>\n</ul>\n<h3>Augmentation / transform</h3>\n<ul>\n<li>Crop-level aug: random H/V flip, ColorJitter</li>\n<li>Per-tile transform: Resize(<strong>img_size=448</strong>), Normalize</li>\n<li>Tile dropout: Dropout for some tiles</li>\n</ul>\n<h2>2. Data split strategy</h2>\n<h3>StratifiedGroupKFold</h3>\n<ul>\n<li><strong>Group key</strong>: <code>Sampling_Date + State</code></li>\n<li><strong>Stratify</strong>: After quantile binning <code>Dry_Total_g</code> using <code>qcut(n_bins=5)</code>, stratify</li>\n</ul>\n<p>We used 4 folds, and the cross-validation was stable between folds.</p>\n<h2>3. Method</h2>\n<h3>3.1 Backbone + tile feature extraction</h3>\n<ul>\n<li>Backbone: <code>timm/vit_huge_plus_patch16_dinov3...</code> (<code>num_classes=0</code>, <code>global_pool=avg</code>)</li>\n<li>Input: <code>(B, 32, C, H, W)</code> → reshape <code>(B*32, C, H, W)</code> → backbone → <code>(B, 32, F)</code></li>\n</ul>\n<h3>3.2 2D positional embedding + multi-head MIL attention pooling</h3>\n<ul>\n<li>Global spatial information is injected by adding 2D positional embeddings <code>(row_emb + col_emb)</code>to the tile features.</li>\n<li>The attention scorer computes softmax weights over the tile dimension (per head) and aggregates the weighted features.</li>\n<li>An output projection maps the result back to <code>(B, F)</code>, keeping the downstream heads simple.</li>\n</ul>\n<h3>3.3 Heads: main(3) + aux(DD/DC)</h3>\n<ul>\n<li><strong>Main head</strong>: Predicts the three primary targets <code>(DG, GDM, DT)</code>.</li>\n<li><strong>Aux heads</strong>: Independently predict <code>DD</code> and <code>DC</code> (with ReLU to prevent negative values).<ul>\n<li>Not used during inference by default.</li></ul></li>\n</ul>\n<h3>3.4 Postprocess + 5-target reconstruction</h3>\n<ul>\n<li>Enforce constraints:<ul>\n<li><code>DG ≥ 0</code>, <code>GDM ≥ DG</code>, <code>DT ≥ GDM</code></li></ul></li>\n<li>5-target reconstruction:<ul>\n<li><code>DD = DT - GDM</code></li>\n<li><code>DC = GDM - DG</code></li></ul></li>\n</ul>\n<h3>3.5 Loss</h3>\n<ul>\n<li><strong>Base</strong>: <code>SmoothL1Loss</code> applied to the 3 main targets.</li>\n<li><strong>R² Surrogate Loss</strong>:<ul>\n<li>Normalized MSE based on SST calculated from the train split.</li>\n<li>Importance weights: [0.1, 0.2, 1.0] for <code>(DG, GDM, DT)</code>.</li></ul></li>\n<li><strong>Hybrid</strong>:<ul>\n<li><code>loss = alpha * SmoothL1 + (1-alpha) * R²_surrogate</code> (with <code>alpha=0.5</code>).</li>\n<li>Empirically, <code>SmoothL1</code> contributed to better training stability.</li></ul></li>\n</ul>\n<h3>3.6 Aux + consistency + log loss</h3>\n<ul>\n<li><strong>Aux</strong>: SmoothL1 on DD/DC.</li>\n<li><strong>Log-space L1</strong>: Improves stability for large-scale targets.</li>\n<li><strong>Consistency Loss</strong> (<code>consis_w=0.2</code>): Enforces alignment between aux predictions and reconstructed values:<ul>\n<li>Induce <code>DD_aux_pred</code> to match <code>(DT−GDM)</code> and <code>DC_aux_pred</code> to match <code>(GDM−DG)</code>.</li></ul></li>\n</ul>\n<h3>3.7 Train-time meta teacher (LUPI) — knowledge distillation</h3>\n<ul>\n<li><strong>Meta input</strong>: NDVI, Height, State/Species, Month + quality meta(sharpness/noise/cluster)</li>\n<li><strong>Teacher predicts ratios (sigmoid)</strong>:<ul>\n<li><code>DG/DT</code>, <code>GDM/DT</code>, <code>DD/DT</code>.</li>\n<li><code>DC/DT</code> is excluded as redundant (derivable from <code>GDM/DT − DG/DT</code>).</li></ul></li>\n<li><strong>Distillation</strong>: Teacher outputs are supervised by target ratios, while student ratios are distilled from the teacher's predictions.</li>\n<li><strong>Absolute DD</strong>: We also separately supervise and distill the absolute DD value (<code>teacher_dd</code>, <code>out_dim=1</code>).</li>\n</ul>\n<h3>3.8 Two-stage fine-tuning</h3>\n<ul>\n<li><strong>Stage 1</strong>: Unfreeze last 1 block (<code>lr=1e-3</code>) to stably train the head, MIL, and meta components.</li>\n<li><strong>Stage 2</strong>: Unfreeze last 3 blocks (<code>lr=1e-4</code>) to fine-tune and push the performance ceiling.</li>\n</ul>\n<h2>4. Results</h2>\n<ul>\n<li><strong>Best Single Model:</strong><ul>\n<li><strong>CV (SGKF 4-fold)</strong>: <strong>0.7615</strong></li>\n<li><strong>Public/Private LB</strong>: <strong>0.78/0.62</strong></li></ul></li>\n</ul>\n<h3>Table: Single-model milestone progression (Baseline → Best)</h3>\n<table>\n<thead>\n<tr>\n<th>Step</th>\n<th>Tiles / img_size</th>\n<th>Key change (what improved)</th>\n<th>Mean CV<br>(4-fold)</th>\n<th>Full OOF</th>\n<th>Public LB</th>\n<th>Δ Full OOF vs Reference†</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Reference†</td>\n<td>–</td>\n<td>early Lightning baseline</td>\n<td>0.5802</td>\n<td>0.6357</td>\n<td>0.64</td>\n<td>+0.0000 (<strong>+0.0%</strong>)</td>\n</tr>\n<tr>\n<td>Baseline (comparable)</td>\n<td>12 / 512</td>\n<td>TileAttentionPool</td>\n<td>0.6972</td>\n<td>0.7044</td>\n<td>0.76</td>\n<td>+0.0687 (<strong>+10.8%</strong>)</td>\n</tr>\n<tr>\n<td>+ Pooling</td>\n<td>12 / 512</td>\n<td>Multi-head MIL pooling</td>\n<td>0.7165</td>\n<td>0.7328</td>\n<td>0.76</td>\n<td>+0.0971 (<strong>+15.3%</strong>)</td>\n</tr>\n<tr>\n<td>+ Tiles / scale</td>\n<td>32 / 256</td>\n<td>32 tiles</td>\n<td>0.7224</td>\n<td>0.7306</td>\n<td>0.77</td>\n<td>+0.1144 (<strong>+18.0%</strong>)</td>\n</tr>\n<tr>\n<td>+ Quality meta</td>\n<td>32 / 256</td>\n<td>+ quality meta (q_sharpness/q_noise/cluster)</td>\n<td>0.7369</td>\n<td>0.7509</td>\n<td>0.77</td>\n<td>+0.1152 (<strong>+18.1%</strong>)</td>\n</tr>\n<tr>\n<td><strong>Best single</strong></td>\n<td><strong>32 / 448</strong></td>\n<td><strong>2D pos + MH-MIL + quality meta + R² loss</strong></td>\n<td><strong>0.7468</strong></td>\n<td><strong>0.7615</strong></td>\n<td><strong>0.78</strong></td>\n<td><strong>+0.1258 (+19.8%)</strong></td>\n</tr>\n</tbody>\n</table>\n<p><em>Mean CV and Full OOF are based on the same 4-fold pipeline. Δ(%) indicates the improvement in Full OOF compared to the Reference†.</em></p>\n<h2>5. Discussion &amp; Retrospective</h2>\n<p>While our solution achieved 6th place on the Public LB, we experienced a significant shake-up, finishing 129th on the Private LB. Below is a summary of our approach's strengths and our retrospective analysis on the drop.</p>\n<h3>What Worked</h3>\n<ul>\n<li><strong>2D Positional Embedding + MIL</strong>: Explicitly modeling the spatial relationship of tiles with 2D embeddings significantly outperformed simple averaging or concatenation (LB 0.71 → 0.75).</li>\n<li><strong>Target Reconstruction Strategy</strong>: Focusing on the three highly correlated \"green biomass\" targets (<code>DG</code>, <code>GDM</code>, <code>DT</code>) and reconstructing the targets (<code>DD</code>, <code>DC</code>) proved effective for ensuring physical constraints (DG &lt;= GDM &lt;= DT) and stabilizing convergence.</li>\n<li><strong>Metric-aligned Training</strong>: Mixing the R² surrogate loss helped align the optimization direction with the competition metric.</li>\n</ul>\n<h3>Retrospective</h3>\n<p>The Shake-up (Public 6th → Private 129th) I suspect two main factors contributed to the lack of generalization on the Private set:</p>\n<ol>\n<li><strong>Over-reliance on Meta-Teacher (LUPI)</strong>: We utilized a Meta-Teacher to distill metadata (NDVI, Season, etc.) into the vision model. While this boosted CV and Public LB, it likely introduced a <strong>bias towards the training/public set's metadata distribution</strong>. As noted in our risks, if the metadata distribution shifted significantly in the Private set, the \"internalized priors\" from the teacher might have harmed the model's robustness.</li>\n<li><strong>CV Strategy vs. Distribution Shift</strong>: We used StratifiedGroupKFold (grouped by Date+State, stratified by Target bins) to maintain a stable CV-LB correlation. While this strategy successfully prevented leakage from same-day samples, it may not have been strict enough to simulate the domain shift present in the Private test set.</li>\n</ol>\n<h2>6. Conclusion</h2>\n<p>Our solution demonstrates that combining <strong>2D positional embeddings</strong> with <strong>MIL pooling</strong> is effective for high-resolution biomass estimation. However, the significant shake-up highlights the risk of <strong>over-fitting to metadata priors via LUPI</strong>. While distilling privileged information boosts performance on similar distributions, it requires more rigorous validation to ensure robustness against domain shifts in unseen data.</p>",
  "messages": [
    {
      "id": "3398702",
      "postDate": "01/29/2026 15:07:20",
      "content": "<h1>TL;DR</h1>\n<p>Our approach reconstructs the input image into a 32-tile grid and learns the global representation using ViT + 2D positional embeddings + Multi-head MIL attention pooling. Instead of predicting all 5 targets directly, we predict the 3 main targets (<code>DG</code>, <code>GDM</code>, <code>DT</code>) and physically reconstruct the remaining two (<code>DD</code>, <code>DC</code>) to align with the metric. We also utilized LUPI (Learning Using Privileged Information) via a meta-teacher to distill knowledge from train-only metadata into the vision model, enabling image-only inference.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F13143896%2F3faf64ba4b9134cb37becc3432355853%2Fimage%20(2).png?generation=1769698780181835&amp;alt=media\" alt=\"Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling\">\n<strong>Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling</strong></p>\n<h2>1. Data preprocessing</h2>\n<h3>Features</h3>\n<ul>\n<li><strong>Meta</strong>: <code>Pre_GSHH_NDVI</code>, <code>Height_Ave_cm</code>, <code>Sampling_Date</code>, <code>State</code>, <code>Species</code></li>\n<li><strong>Derived</strong><ul>\n<li><code>Month</code>: extracted from <code>Sampling_Date</code></li>\n<li><code>State_encoded</code>, <code>Species_encoded</code>: label encoding</li></ul></li>\n</ul>\n<h3>Quality meta (train-time meta feature)</h3>\n<p>(Ref. <a href=\"https://www.kaggle.com/competitions/csiro-biomass/discussion/667537\" target=\"_blank\">https://www.kaggle.com/competitions/csiro-biomass/discussion/667537</a>)</p>\n<ul>\n<li><code>q_sharpness</code>: Laplacian variance (sharpness)</li>\n<li><code>q_noise</code>: residual mean after Gaussian blur (noise proxy)</li>\n<li><code>q_device_cluster</code>: <code>StandardScaler + KMeans(n_clusters=2)</code></li>\n</ul>\n<h3>32-tile input construction (4×8 global grid)</h3>\n<ul>\n<li>Original image: <strong>2000×1000</strong></li>\n<li>Crops (each 1000×1000)<ul>\n<li>Left: x 0–1000</li>\n<li>Middle: x 500–1500</li>\n<li>Right: x 1000–2000</li></ul></li>\n<li>Build 32 tiles of 250×250 on a global 4×8 grid:<ul>\n<li>global col 0..1: from <strong>Left</strong> (local col 0..1) → 8 tiles</li>\n<li>global col 2..5: from <strong>Middle</strong> (local col 0..3) → 16 tiles</li>\n<li>global col 6..7: from <strong>Right</strong> (local col 2..3) → 8 tiles\n→ total 32 tiles (row-major order)</li></ul></li>\n</ul>\n<h3>Augmentation / transform</h3>\n<ul>\n<li>Crop-level aug: random H/V flip, ColorJitter</li>\n<li>Per-tile transform: Resize(<strong>img_size=448</strong>), Normalize</li>\n<li>Tile dropout: Dropout for some tiles</li>\n</ul>\n<h2>2. Data split strategy</h2>\n<h3>StratifiedGroupKFold</h3>\n<ul>\n<li><strong>Group key</strong>: <code>Sampling_Date + State</code></li>\n<li><strong>Stratify</strong>: After quantile binning <code>Dry_Total_g</code> using <code>qcut(n_bins=5)</code>, stratify</li>\n</ul>\n<p>We used 4 folds, and the cross-validation was stable between folds.</p>\n<h2>3. Method</h2>\n<h3>3.1 Backbone + tile feature extraction</h3>\n<ul>\n<li>Backbone: <code>timm/vit_huge_plus_patch16_dinov3...</code> (<code>num_classes=0</code>, <code>global_pool=avg</code>)</li>\n<li>Input: <code>(B, 32, C, H, W)</code> → reshape <code>(B*32, C, H, W)</code> → backbone → <code>(B, 32, F)</code></li>\n</ul>\n<h3>3.2 2D positional embedding + multi-head MIL attention pooling</h3>\n<ul>\n<li>Global spatial information is injected by adding 2D positional embeddings <code>(row_emb + col_emb)</code>to the tile features.</li>\n<li>The attention scorer computes softmax weights over the tile dimension (per head) and aggregates the weighted features.</li>\n<li>An output projection maps the result back to <code>(B, F)</code>, keeping the downstream heads simple.</li>\n</ul>\n<h3>3.3 Heads: main(3) + aux(DD/DC)</h3>\n<ul>\n<li><strong>Main head</strong>: Predicts the three primary targets <code>(DG, GDM, DT)</code>.</li>\n<li><strong>Aux heads</strong>: Independently predict <code>DD</code> and <code>DC</code> (with ReLU to prevent negative values).<ul>\n<li>Not used during inference by default.</li></ul></li>\n</ul>\n<h3>3.4 Postprocess + 5-target reconstruction</h3>\n<ul>\n<li>Enforce constraints:<ul>\n<li><code>DG ≥ 0</code>, <code>GDM ≥ DG</code>, <code>DT ≥ GDM</code></li></ul></li>\n<li>5-target reconstruction:<ul>\n<li><code>DD = DT - GDM</code></li>\n<li><code>DC = GDM - DG</code></li></ul></li>\n</ul>\n<h3>3.5 Loss</h3>\n<ul>\n<li><strong>Base</strong>: <code>SmoothL1Loss</code> applied to the 3 main targets.</li>\n<li><strong>R² Surrogate Loss</strong>:<ul>\n<li>Normalized MSE based on SST calculated from the train split.</li>\n<li>Importance weights: [0.1, 0.2, 1.0] for <code>(DG, GDM, DT)</code>.</li></ul></li>\n<li><strong>Hybrid</strong>:<ul>\n<li><code>loss = alpha * SmoothL1 + (1-alpha) * R²_surrogate</code> (with <code>alpha=0.5</code>).</li>\n<li>Empirically, <code>SmoothL1</code> contributed to better training stability.</li></ul></li>\n</ul>\n<h3>3.6 Aux + consistency + log loss</h3>\n<ul>\n<li><strong>Aux</strong>: SmoothL1 on DD/DC.</li>\n<li><strong>Log-space L1</strong>: Improves stability for large-scale targets.</li>\n<li><strong>Consistency Loss</strong> (<code>consis_w=0.2</code>): Enforces alignment between aux predictions and reconstructed values:<ul>\n<li>Induce <code>DD_aux_pred</code> to match <code>(DT−GDM)</code> and <code>DC_aux_pred</code> to match <code>(GDM−DG)</code>.</li></ul></li>\n</ul>\n<h3>3.7 Train-time meta teacher (LUPI) — knowledge distillation</h3>\n<ul>\n<li><strong>Meta input</strong>: NDVI, Height, State/Species, Month + quality meta(sharpness/noise/cluster)</li>\n<li><strong>Teacher predicts ratios (sigmoid)</strong>:<ul>\n<li><code>DG/DT</code>, <code>GDM/DT</code>, <code>DD/DT</code>.</li>\n<li><code>DC/DT</code> is excluded as redundant (derivable from <code>GDM/DT − DG/DT</code>).</li></ul></li>\n<li><strong>Distillation</strong>: Teacher outputs are supervised by target ratios, while student ratios are distilled from the teacher's predictions.</li>\n<li><strong>Absolute DD</strong>: We also separately supervise and distill the absolute DD value (<code>teacher_dd</code>, <code>out_dim=1</code>).</li>\n</ul>\n<h3>3.8 Two-stage fine-tuning</h3>\n<ul>\n<li><strong>Stage 1</strong>: Unfreeze last 1 block (<code>lr=1e-3</code>) to stably train the head, MIL, and meta components.</li>\n<li><strong>Stage 2</strong>: Unfreeze last 3 blocks (<code>lr=1e-4</code>) to fine-tune and push the performance ceiling.</li>\n</ul>\n<h2>4. Results</h2>\n<ul>\n<li><strong>Best Single Model:</strong><ul>\n<li><strong>CV (SGKF 4-fold)</strong>: <strong>0.7615</strong></li>\n<li><strong>Public/Private LB</strong>: <strong>0.78/0.62</strong></li></ul></li>\n</ul>\n<h3>Table: Single-model milestone progression (Baseline → Best)</h3>\n<table>\n<thead>\n<tr>\n<th>Step</th>\n<th>Tiles / img_size</th>\n<th>Key change (what improved)</th>\n<th>Mean CV<br>(4-fold)</th>\n<th>Full OOF</th>\n<th>Public LB</th>\n<th>Δ Full OOF vs Reference†</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Reference†</td>\n<td>–</td>\n<td>early Lightning baseline</td>\n<td>0.5802</td>\n<td>0.6357</td>\n<td>0.64</td>\n<td>+0.0000 (<strong>+0.0%</strong>)</td>\n</tr>\n<tr>\n<td>Baseline (comparable)</td>\n<td>12 / 512</td>\n<td>TileAttentionPool</td>\n<td>0.6972</td>\n<td>0.7044</td>\n<td>0.76</td>\n<td>+0.0687 (<strong>+10.8%</strong>)</td>\n</tr>\n<tr>\n<td>+ Pooling</td>\n<td>12 / 512</td>\n<td>Multi-head MIL pooling</td>\n<td>0.7165</td>\n<td>0.7328</td>\n<td>0.76</td>\n<td>+0.0971 (<strong>+15.3%</strong>)</td>\n</tr>\n<tr>\n<td>+ Tiles / scale</td>\n<td>32 / 256</td>\n<td>32 tiles</td>\n<td>0.7224</td>\n<td>0.7306</td>\n<td>0.77</td>\n<td>+0.1144 (<strong>+18.0%</strong>)</td>\n</tr>\n<tr>\n<td>+ Quality meta</td>\n<td>32 / 256</td>\n<td>+ quality meta (q_sharpness/q_noise/cluster)</td>\n<td>0.7369</td>\n<td>0.7509</td>\n<td>0.77</td>\n<td>+0.1152 (<strong>+18.1%</strong>)</td>\n</tr>\n<tr>\n<td><strong>Best single</strong></td>\n<td><strong>32 / 448</strong></td>\n<td><strong>2D pos + MH-MIL + quality meta + R² loss</strong></td>\n<td><strong>0.7468</strong></td>\n<td><strong>0.7615</strong></td>\n<td><strong>0.78</strong></td>\n<td><strong>+0.1258 (+19.8%)</strong></td>\n</tr>\n</tbody>\n</table>\n<p><em>Mean CV and Full OOF are based on the same 4-fold pipeline. Δ(%) indicates the improvement in Full OOF compared to the Reference†.</em></p>\n<h2>5. Discussion &amp; Retrospective</h2>\n<p>While our solution achieved 6th place on the Public LB, we experienced a significant shake-up, finishing 129th on the Private LB. Below is a summary of our approach's strengths and our retrospective analysis on the drop.</p>\n<h3>What Worked</h3>\n<ul>\n<li><strong>2D Positional Embedding + MIL</strong>: Explicitly modeling the spatial relationship of tiles with 2D embeddings significantly outperformed simple averaging or concatenation (LB 0.71 → 0.75).</li>\n<li><strong>Target Reconstruction Strategy</strong>: Focusing on the three highly correlated \"green biomass\" targets (<code>DG</code>, <code>GDM</code>, <code>DT</code>) and reconstructing the targets (<code>DD</code>, <code>DC</code>) proved effective for ensuring physical constraints (DG &lt;= GDM &lt;= DT) and stabilizing convergence.</li>\n<li><strong>Metric-aligned Training</strong>: Mixing the R² surrogate loss helped align the optimization direction with the competition metric.</li>\n</ul>\n<h3>Retrospective</h3>\n<p>The Shake-up (Public 6th → Private 129th) I suspect two main factors contributed to the lack of generalization on the Private set:</p>\n<ol>\n<li><strong>Over-reliance on Meta-Teacher (LUPI)</strong>: We utilized a Meta-Teacher to distill metadata (NDVI, Season, etc.) into the vision model. While this boosted CV and Public LB, it likely introduced a <strong>bias towards the training/public set's metadata distribution</strong>. As noted in our risks, if the metadata distribution shifted significantly in the Private set, the \"internalized priors\" from the teacher might have harmed the model's robustness.</li>\n<li><strong>CV Strategy vs. Distribution Shift</strong>: We used StratifiedGroupKFold (grouped by Date+State, stratified by Target bins) to maintain a stable CV-LB correlation. While this strategy successfully prevented leakage from same-day samples, it may not have been strict enough to simulate the domain shift present in the Private test set.</li>\n</ol>\n<h2>6. Conclusion</h2>\n<p>Our solution demonstrates that combining <strong>2D positional embeddings</strong> with <strong>MIL pooling</strong> is effective for high-resolution biomass estimation. However, the significant shake-up highlights the risk of <strong>over-fitting to metadata priors via LUPI</strong>. While distilling privileged information boosts performance on similar distributions, it requires more rigorous validation to ensure robustness against domain shifts in unseen data.</p>",
      "rawMarkdown": "# TL;DR\nOur approach reconstructs the input image into a 32-tile grid and learns the global representation using ViT + 2D positional embeddings + Multi-head MIL attention pooling. Instead of predicting all 5 targets directly, we predict the 3 main targets (`DG`, `GDM`, `DT`) and physically reconstruct the remaining two (`DD`, `DC`) to align with the metric. We also utilized LUPI (Learning Using Privileged Information) via a meta-teacher to distill knowledge from train-only metadata into the vision model, enabling image-only inference.\n\n\n![Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F13143896%2F3faf64ba4b9134cb37becc3432355853%2Fimage%20(2).png?generation=1769698780181835&alt=media)\n**Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling**\n\n## 1. Data preprocessing\n\n### Features\n\n- **Meta**: `Pre_GSHH_NDVI`, `Height_Ave_cm`, `Sampling_Date`, `State`, `Species`\n- **Derived**\n    - `Month`: extracted from `Sampling_Date`\n    - `State_encoded`, `Species_encoded`: label encoding\n\n### Quality meta (train-time meta feature)\n(Ref. https://www.kaggle.com/competitions/csiro-biomass/discussion/667537)\n\n- `q_sharpness`: Laplacian variance (sharpness)\n- `q_noise`: residual mean after Gaussian blur (noise proxy)\n- `q_device_cluster`: `StandardScaler + KMeans(n_clusters=2)`\n\n### 32-tile input construction (4×8 global grid)\n- Original image: **2000×1000**\n- Crops (each 1000×1000)\n    - Left: x 0–1000\n    - Middle: x 500–1500\n    - Right: x 1000–2000\n- Build 32 tiles of 250×250 on a global 4×8 grid:\n    - global col 0..1: from **Left** (local col 0..1) → 8 tiles\n    - global col 2..5: from **Middle** (local col 0..3) → 16 tiles\n    - global col 6..7: from **Right** (local col 2..3) → 8 tiles\n        → total 32 tiles (row-major order)\n\n### Augmentation / transform\n- Crop-level aug: random H/V flip, ColorJitter\n- Per-tile transform: Resize(**img_size=448**), Normalize\n- Tile dropout: Dropout for some tiles\n\n## 2. Data split strategy\n\n### StratifiedGroupKFold\n- **Group key**: `Sampling_Date + State`\n- **Stratify**: After quantile binning `Dry_Total_g` using `qcut(n_bins=5)`, stratify\n\nWe used 4 folds, and the cross-validation was stable between folds.\n\n## 3. Method\n### 3.1 Backbone + tile feature extraction\n- Backbone: `timm/vit_huge_plus_patch16_dinov3...` (`num_classes=0`, `global_pool=avg`)\n- Input: `(B, 32, C, H, W)` → reshape `(B*32, C, H, W)` → backbone → `(B, 32, F)`\n\n### 3.2 2D positional embedding + multi-head MIL attention pooling\n- Global spatial information is injected by adding 2D positional embeddings `(row_emb + col_emb) `to the tile features.\n- The attention scorer computes softmax weights over the tile dimension (per head) and aggregates the weighted features.\n- An output projection maps the result back to `(B, F)`, keeping the downstream heads simple.\n\n### 3.3 Heads: main(3) + aux(DD/DC)\n\n- **Main head**: Predicts the three primary targets `(DG, GDM, DT)`.\n- **Aux heads**: Independently predict `DD` and `DC` (with ReLU to prevent negative values).\n   - Not used during inference by default.\n\n### 3.4 Postprocess + 5-target reconstruction\n\n- Enforce constraints:\n    - `DG ≥ 0`, `GDM ≥ DG`, `DT ≥ GDM`\n- 5-target reconstruction:\n    - `DD = DT - GDM`\n    - `DC = GDM - DG`\n\n### 3.5 Loss\n\n- **Base**: `SmoothL1Loss` applied to the 3 main targets.\n- **R² Surrogate Loss**:\n   - Normalized MSE based on SST calculated from the train split.\n   - Importance weights: [0.1, 0.2, 1.0] for `(DG, GDM, DT)`.\n- **Hybrid**:\n   - `loss = alpha * SmoothL1 + (1-alpha) * R²_surrogate` (with `alpha=0.5`).\n   - Empirically, `SmoothL1` contributed to better training stability.\n\n\n### 3.6 Aux + consistency + log loss\n\n- **Aux**: SmoothL1 on DD/DC.\n- **Log-space L1**: Improves stability for large-scale targets.\n- **Consistency Loss** (`consis_w=0.2`): Enforces alignment between aux predictions and reconstructed values:\n   -  Induce `DD_aux_pred` to match `(DT−GDM)` and `DC_aux_pred` to match `(GDM−DG)`.\n\n### 3.7 Train-time meta teacher (LUPI) — knowledge distillation\n\n- **Meta input**: NDVI, Height, State/Species, Month + quality meta(sharpness/noise/cluster)\n- **Teacher predicts ratios (sigmoid)**:\n   - `DG/DT`, `GDM/DT`, `DD/DT`.\n   - `DC/DT` is excluded as redundant (derivable from `GDM/DT − DG/DT`).\n- **Distillation**: Teacher outputs are supervised by target ratios, while student ratios are distilled from the teacher's predictions.\n- **Absolute DD**: We also separately supervise and distill the absolute DD value (`teacher_dd`, `out_dim=1`).\n\n### 3.8 Two-stage fine-tuning\n\n- **Stage 1**: Unfreeze last 1 block (`lr=1e-3`) to stably train the head, MIL, and meta components.\n- **Stage 2**: Unfreeze last 3 blocks (`lr=1e-4`) to fine-tune and push the performance ceiling.\n\n## 4. Results\n- **Best Single Model:**\n    - **CV (SGKF 4-fold)**: **0.7615**\n    - **Public/Private LB**: **0.78/0.62**\n\n### Table: Single-model milestone progression (Baseline → Best)\n\n| Step | Tiles / img_size | Key change (what improved) | Mean CV<br>(4-fold) | Full OOF | Public LB | Δ Full OOF vs Reference† |\n| :--- | :--- | :--- | :--- | :--- | :--- | :--- |\n| Reference† | – | early Lightning baseline | 0.5802 | 0.6357 | 0.64 | +0.0000 (**+0.0%**) |\n| Baseline (comparable) | 12 / 512 | TileAttentionPool | 0.6972 | 0.7044 | 0.76 | +0.0687 (**+10.8%**) |\n| + Pooling | 12 / 512 | Multi-head MIL pooling | 0.7165 | 0.7328 | 0.76 | +0.0971 (**+15.3%**) |\n| + Tiles / scale | 32 / 256 | 32 tiles | 0.7224 | 0.7306 | 0.77 | +0.1144 (**+18.0%**) |\n| + Quality meta | 32 / 256 | + quality meta (q_sharpness/q_noise/cluster) | 0.7369 | 0.7509 | 0.77 | +0.1152 (**+18.1%**) |\n| **Best single** | **32 / 448** | **2D pos + MH-MIL + quality meta + R² loss** | **0.7468** | **0.7615** | **0.78** | **+0.1258 (+19.8%)** |\n\n*Mean CV and Full OOF are based on the same 4-fold pipeline. Δ(%) indicates the improvement in Full OOF compared to the Reference†.*\n\n## 5. Discussion & Retrospective\nWhile our solution achieved 6th place on the Public LB, we experienced a significant shake-up, finishing 129th on the Private LB. Below is a summary of our approach's strengths and our retrospective analysis on the drop.\n\n### What Worked\n- **2D Positional Embedding + MIL**: Explicitly modeling the spatial relationship of tiles with 2D embeddings significantly outperformed simple averaging or concatenation (LB 0.71 → 0.75).\n- **Target Reconstruction Strategy**: Focusing on the three highly correlated \"green biomass\" targets (`DG`, `GDM`, `DT`) and reconstructing the targets (`DD`, `DC`) proved effective for ensuring physical constraints (DG <= GDM <= DT) and stabilizing convergence.\n- **Metric-aligned Training**: Mixing the R² surrogate loss helped align the optimization direction with the competition metric.\n\n### Retrospective\nThe Shake-up (Public 6th → Private 129th) I suspect two main factors contributed to the lack of generalization on the Private set:\n1. **Over-reliance on Meta-Teacher (LUPI)**: We utilized a Meta-Teacher to distill metadata (NDVI, Season, etc.) into the vision model. While this boosted CV and Public LB, it likely introduced a **bias towards the training/public set's metadata distribution**. As noted in our risks, if the metadata distribution shifted significantly in the Private set, the \"internalized priors\" from the teacher might have harmed the model's robustness.\n2. **CV Strategy vs. Distribution Shift**: We used StratifiedGroupKFold (grouped by Date+State, stratified by Target bins) to maintain a stable CV-LB correlation. While this strategy successfully prevented leakage from same-day samples, it may not have been strict enough to simulate the domain shift present in the Private test set.\n\n## 6. Conclusion\nOur solution demonstrates that combining **2D positional embeddings** with **MIL pooling** is effective for high-resolution biomass estimation. However, the significant shake-up highlights the risk of **over-fitting to metadata priors via LUPI**. While distilling privileged information boosts performance on similar distributions, it requires more rigorous validation to ensure robustness against domain shifts in unseen data.",
      "votes": null
    },
    {
      "id": "3398817",
      "postDate": "01/29/2026 18:34:06",
      "content": "<pre><code>32-tile input construction (4×8 global grid)\n</code></pre>\n<p>Feels like this splitting the image into independent tiles was the reason for the shake down. We observed the same. The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58. 500/500 250/250 100/100 or even 16/16 patch token level were all bad (with more tiles, the public and private moved in opposite directions)</p>\n<p>The single image which performed trash on both CV and public LB did better on private. Which I felt is quite surprising since I thought 2000x1000 -&gt; 448x448 had significant information loss 😐</p>\n<p>Edit: the 0.68 wasn’t even with Dino so it’s insane 😂</p>",
      "rawMarkdown": "```\n32-tile input construction (4×8 global grid)\n```\n\nFeels like this splitting the image into independent tiles was the reason for the shake down. We observed the same. The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58. 500/500 250/250 100/100 or even 16/16 patch token level were all bad (with more tiles, the public and private moved in opposite directions)\n\nThe single image which performed trash on both CV and public LB did better on private. Which I felt is quite surprising since I thought 2000x1000 -> 448x448 had significant information loss 😐\n\nEdit: the 0.68 wasn’t even with Dino so it’s insane 😂",
      "votes": null
    },
    {
      "id": "3398939",
      "postDate": "01/29/2026 23:55:49",
      "content": "<p>Thanks for the great insights.</p>\n<blockquote>\n  <p>The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58.</p>\n</blockquote>\n<p>I learned something new here. Empirically, it seems the tile-based approach hurt private LB performance.\nDo you have any tips on how we could have noticed or expected this before the private scores came out? In my experiments, CV and public LB were pretty well correlated.</p>",
      "rawMarkdown": "Thanks for the great insights.\n\n> The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58.\n\nI learned something new here. Empirically, it seems the tile-based approach hurt private LB performance.\nDo you have any tips on how we could have noticed or expected this before the private scores came out? In my experiments, CV and public LB were pretty well correlated.",
      "votes": null
    },
    {
      "id": "3398948",
      "postDate": "01/30/2026 00:31:44",
      "content": "<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2832287%2F30633138e205f82348fb403dabbb2ead%2FScreenshot%202026-01-30%20at%2007.29.49.png?generation=1769733019317622&amp;alt=media\" alt=\"\">\nOh, for me, 8 tiles is much worse than 2 tiles on the private leaderboard, even though the public scores are the same.</p>",
      "rawMarkdown": "![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2832287%2F30633138e205f82348fb403dabbb2ead%2FScreenshot%202026-01-30%20at%2007.29.49.png?generation=1769733019317622&alt=media)\nOh, for me, 8 tiles is much worse than 2 tiles on the private leaderboard, even though the public scores are the same.",
      "votes": null
    },
    {
      "id": "3398985",
      "postDate": "01/30/2026 02:29:19",
      "content": "<p>I guess lots of the top teams on the public leaderboard used the same tile-level chunking method, many of them dropped by similar magnitude.</p>\n<p>I wonder is it because overprediction of clovers?</p>",
      "rawMarkdown": "I guess lots of the top teams on the public leaderboard used the same tile-level chunking method, many of them dropped by similar magnitude.\n\nI wonder is it because overprediction of clovers?",
      "votes": null
    },
    {
      "id": "3399013",
      "postDate": "01/30/2026 03:56:45",
      "content": "<p>I did not employ any tiles-level feature aggregation, predict each biomass targets for each tile then sum all (hopefully prevent overfitting). But more tiles, CV decreases which I can't find way to recover the accuracy. I mean, global context is still important, model just looking at overall shape/semantic and guess, so DINOv3 <strong>VIT (but not ConvNeXT)</strong>  dominates this competition.\nHere are some local CV results on frozen backbone, did not submit to LB:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F10254700%2F52158752b421ab748f27ac6e6f8bd2b9%2FScreenshot%202026-01-30%20at%2010.52.03.png?generation=1769745330703345&amp;alt=media\" alt=\"\"></p>",
      "rawMarkdown": "I did not employ any tiles-level feature aggregation, predict each biomass targets for each tile then sum all (hopefully prevent overfitting). But more tiles, CV decreases which I can't find way to recover the accuracy. I mean, global context is still important, model just looking at overall shape/semantic and guess, so DINOv3 **VIT (but not ConvNeXT)**  dominates this competition.\nHere are some local CV results on frozen backbone, did not submit to LB:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F10254700%2F52158752b421ab748f27ac6e6f8bd2b9%2FScreenshot%202026-01-30%20at%2010.52.03.png?generation=1769745330703345&alt=media)",
      "votes": null
    },
    {
      "id": "3399020",
      "postDate": "01/30/2026 04:13:04",
      "content": "<p>Yeah, when I submitted the 8-tile solution to the public leaderboard, the score dropped a bit, which is why I didn’t choose the 8-tile solution as my main approach. I used the 2-tile solution as the primary one and added the 8-tile solution into the ensemble, but it actually dragged my ensemble score down 😅</p>",
      "rawMarkdown": "Yeah, when I submitted the 8-tile solution to the public leaderboard, the score dropped a bit, which is why I didn’t choose the 8-tile solution as my main approach. I used the 2-tile solution as the primary one and added the 8-tile solution into the ensemble, but it actually dragged my ensemble score down 😅",
      "votes": null
    },
    {
      "id": "3399031",
      "postDate": "01/30/2026 04:46:37",
      "content": "<p>Sorry to see you guys took such a big drop.  Lots of things to learn about small sample size model building.  Have not looked at the results the hosts got with meta only data a decade ago, but seems like vision did not add as much as they might have hoped.</p>\n<p>The images were pretty crappy - hopefully if you do a vision model you don't generate low quality images and hope they \"generalize\" well.  The description of how the weights of each of the labels were done also seemed pretty sketchy to me.  Probably involved in several thousand experiments over my working years and I made the comment at the start of this competition that it was a pretty poor dataset.  </p>\n<p>Regardless did learn a lot myself about modeling with such a small number of images and had fun most of the 3 months.  Ended up spending almost $3000 to upgrade my computes, so really committed to getting an ROI over the rest of this year :)</p>",
      "rawMarkdown": "Sorry to see you guys took such a big drop.  Lots of things to learn about small sample size model building.  Have not looked at the results the hosts got with meta only data a decade ago, but seems like vision did not add as much as they might have hoped.\n\nThe images were pretty crappy - hopefully if you do a vision model you don't generate low quality images and hope they \"generalize\" well.  The description of how the weights of each of the labels were done also seemed pretty sketchy to me.  Probably involved in several thousand experiments over my working years and I made the comment at the start of this competition that it was a pretty poor dataset.  \n\nRegardless did learn a lot myself about modeling with such a small number of images and had fun most of the 3 months.  Ended up spending almost $3000 to upgrade my computes, so really committed to getting an ROI over the rest of this year :)",
      "votes": null
    },
    {
      "id": "3399274",
      "postDate": "01/30/2026 14:44:36",
      "content": "<p>Ahh was it really tiles ,, we are testing with other folds if it was folds or tiles :( . </p>",
      "rawMarkdown": "Ahh was it really tiles ,, we are testing with other folds if it was folds or tiles :( .",
      "votes": null
    },
    {
      "id": "3399297",
      "postDate": "01/30/2026 15:49:30",
      "content": "<p>In my experiments, 32 tile-base sum apporach improve my lb and cv significantly,  but I think this method lost the global feature. I try many method to fuse local and global information, but still can not get good result. My best PB single model use self-supervised learning and global feature to predict the local tile position, but get low LB.  I still feel that the dataset for this competition might be relatively small, resulting in a certain level of randomness in the outcomes. Moreover, some biomass information cannot be determined solely from images.\nSo, your method might actually be more effective with larger datasets.</p>",
      "rawMarkdown": "In my experiments, 32 tile-base sum apporach improve my lb and cv significantly,  but I think this method lost the global feature. I try many method to fuse local and global information, but still can not get good result. My best PB single model use self-supervised learning and global feature to predict the local tile position, but get low LB.  I still feel that the dataset for this competition might be relatively small, resulting in a certain level of randomness in the outcomes. Moreover, some biomass information cannot be determined solely from images.\nSo, your method might actually be more effective with larger datasets.",
      "votes": null
    },
    {
      "id": "3399310",
      "postDate": "01/30/2026 16:02:33",
      "content": "<p>I also tried to do something like this with 8 patches. I got it upto 0.6,1 but I did not understand ViT in its technical depth yet, so I gave up on this early and moved to a simpler structure. </p>\n<p>I basically tried to create a first transformer structure, which was pretrained and frozen. Then created another transformer layer, which treated the first transformer patch output as input patches and predicted the final feats to which I attached the heads.</p>\n<pre><code>class BioMassViTRegressor(nn.Module):\n    def __init__(\n        self,\n        backbone: str,\n        weights: Optional[Any] = None,\n        num_targets: int = 4,\n        drop_rate: float = 0.3,\n        patch_w: int = 4,\n        patch_h: int = 2,\n        cp: bool = True,\n    ):\n        super().__init__()\n\n        # save the attributes\n        self.num_targets = num_targets\n        self.num_patches = patch_w * patch_h\n        self.cp = cp\n\n        # build initial patch creator for processing high res image\n        self.patchifier = Rearrange(\"n c (h ph) (w pw) -&gt; n (h w) c ph pw\", h=patch_h, w=patch_w)\n\n        # build backbone layers\n        backbone, out_dim = self.build_backbone(backbone, weights=weights)\n        self.backbone = backbone\n\n        # build local patch compressor (Attentive Pooling)\n        self.compressor = PatchCompressor(out_dim)\n\n        # global transformer aggregator (~1.5M params)\n        self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, out_dim))\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=out_dim, nhead=self.num_patches, dim_feedforward=out_dim * 2, dropout=drop_rate, batch_first=True\n        )\n        self.global_transformer = nn.TransformerEncoder(encoder_layer, num_layers=1)\n\n        # 5. Final Regression Head\n        self.head = nn.Sequential(\n            nn.LayerNorm(out_dim),\n            nn.Linear(out_dim, out_dim // 2),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(out_dim // 2, num_targets),\n        )\n\n    def freeze_backbone(self, stem: bool = True, stages: int = 4):\n        # unfreeze everything first\n        for param in self.backbone.parameters():\n            param.requires_grad = True\n\n        # apply stem freeze if requested\n        if stem:\n            for param in self.backbone.patch_embed.parameters():\n                param.requires_grad = False\n\n        # apply stage-wise freeze\n        if stages != 0:\n            for stage_idx in range(stages):\n                block_start_idx = stage_idx * 3\n                for block_idx in range(block_start_idx, block_start_idx + 3):\n                    for param in self.backbone.blocks[block_idx].parameters():\n                        param.requires_grad = False\n\n    def build_backbone(self, backbone: str, weights: Optional[str], **kwargs):\n        if backbone.startswith(\"vit\"):\n            if weights is None:\n                model = timm.create_model(backbone, pretrained=False, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == False:\n                model = timm.create_model(f\"{backbone}.{weights}\", pretrained=True, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == True:\n                model = timm.create_model(backbone, pretrained=None, checkpoint_path=weights, num_classes=0, **kwargs)\n\n            # mask the head and return the backbone\n            model.head = nn.Identity()\n            return model, model.head_hidden_size\n\n        else:\n            raise ValueError(\"Unknown backbone requested\")\n\n    def forward(self, x: torch.Tensor) -&gt; torch.Tensor:\n        batch_size = x.shape[0]\n\n        # create patches and extract patch features\n        patch_feats = []\n        if len(x.shape) == 4:\n            x = self.patchifier(x)\n        for patch_idx in range(self.num_patches):\n            feat_patch = checkpoint(self.backbone.forward_features, x[:, patch_idx, :, :], use_reentrant=False)\n            patch_feats.append(feat_patch)\n\n        # stitching individual patches in one\n        x = patch_feats\n        x = torch.stack(patch_feats, dim=1)\n        x = x.view(-1, x.shape[2], x.shape[3])\n        x = self.compressor(x)\n        x = x.view(batch_size, self.num_patches, -1)\n        x = x + self.pos_embed\n        x = self.global_transformer(x)\n        x = x.mean(dim=1)\n        return self.head(x)\n</code></pre>",
      "rawMarkdown": "I also tried to do something like this with 8 patches. I got it upto 0.6,1 but I did not understand ViT in its technical depth yet, so I gave up on this early and moved to a simpler structure. \n\nI basically tried to create a first transformer structure, which was pretrained and frozen. Then created another transformer layer, which treated the first transformer patch output as input patches and predicted the final feats to which I attached the heads.\n\n```\nclass BioMassViTRegressor(nn.Module):\n    def __init__(\n        self,\n        backbone: str,\n        weights: Optional[Any] = None,\n        num_targets: int = 4,\n        drop_rate: float = 0.3,\n        patch_w: int = 4,\n        patch_h: int = 2,\n        cp: bool = True,\n    ):\n        super().__init__()\n\n        # save the attributes\n        self.num_targets = num_targets\n        self.num_patches = patch_w * patch_h\n        self.cp = cp\n\n        # build initial patch creator for processing high res image\n        self.patchifier = Rearrange(\"n c (h ph) (w pw) -> n (h w) c ph pw\", h=patch_h, w=patch_w)\n\n        # build backbone layers\n        backbone, out_dim = self.build_backbone(backbone, weights=weights)\n        self.backbone = backbone\n\n        # build local patch compressor (Attentive Pooling)\n        self.compressor = PatchCompressor(out_dim)\n\n        # global transformer aggregator (~1.5M params)\n        self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, out_dim))\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=out_dim, nhead=self.num_patches, dim_feedforward=out_dim * 2, dropout=drop_rate, batch_first=True\n        )\n        self.global_transformer = nn.TransformerEncoder(encoder_layer, num_layers=1)\n\n        # 5. Final Regression Head\n        self.head = nn.Sequential(\n            nn.LayerNorm(out_dim),\n            nn.Linear(out_dim, out_dim // 2),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(out_dim // 2, num_targets),\n        )\n\n    def freeze_backbone(self, stem: bool = True, stages: int = 4):\n        # unfreeze everything first\n        for param in self.backbone.parameters():\n            param.requires_grad = True\n\n        # apply stem freeze if requested\n        if stem:\n            for param in self.backbone.patch_embed.parameters():\n                param.requires_grad = False\n\n        # apply stage-wise freeze\n        if stages != 0:\n            for stage_idx in range(stages):\n                block_start_idx = stage_idx * 3\n                for block_idx in range(block_start_idx, block_start_idx + 3):\n                    for param in self.backbone.blocks[block_idx].parameters():\n                        param.requires_grad = False\n\n    def build_backbone(self, backbone: str, weights: Optional[str], **kwargs):\n        if backbone.startswith(\"vit\"):\n            if weights is None:\n                model = timm.create_model(backbone, pretrained=False, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == False:\n                model = timm.create_model(f\"{backbone}.{weights}\", pretrained=True, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == True:\n                model = timm.create_model(backbone, pretrained=None, checkpoint_path=weights, num_classes=0, **kwargs)\n\n            # mask the head and return the backbone\n            model.head = nn.Identity()\n            return model, model.head_hidden_size\n\n        else:\n            raise ValueError(\"Unknown backbone requested\")\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        batch_size = x.shape[0]\n\n        # create patches and extract patch features\n        patch_feats = []\n        if len(x.shape) == 4:\n            x = self.patchifier(x)\n        for patch_idx in range(self.num_patches):\n            feat_patch = checkpoint(self.backbone.forward_features, x[:, patch_idx, :, :], use_reentrant=False)\n            patch_feats.append(feat_patch)\n\n        # stitching individual patches in one\n        x = patch_feats\n        x = torch.stack(patch_feats, dim=1)\n        x = x.view(-1, x.shape[2], x.shape[3])\n        x = self.compressor(x)\n        x = x.view(batch_size, self.num_patches, -1)\n        x = x + self.pos_embed\n        x = self.global_transformer(x)\n        x = x.mean(dim=1)\n        return self.head(x)\n```",
      "votes": null
    }
  ],
  "comments": [
    {
      "id": 3398817,
      "author_name": "yeoyunsianggeremie",
      "author_url": "",
      "post_date": "01/29/2026 18:34:06",
      "content": "<pre><code>32-tile input construction (4×8 global grid)\n</code></pre>\n<p>Feels like this splitting the image into independent tiles was the reason for the shake down. We observed the same. The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58. 500/500 250/250 100/100 or even 16/16 patch token level were all bad (with more tiles, the public and private moved in opposite directions)</p>\n<p>The single image which performed trash on both CV and public LB did better on private. Which I felt is quite surprising since I thought 2000x1000 -&gt; 448x448 had significant information loss 😐</p>\n<p>Edit: the 0.68 wasn’t even with Dino so it’s insane 😂</p>",
      "votes": null,
      "replies": [
        {
          "id": 3398939,
          "author_name": "ayaanjang",
          "author_url": "",
          "post_date": "01/29/2026 23:55:49",
          "content": "<p>Thanks for the great insights.</p>\n<blockquote>\n  <p>The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58.</p>\n</blockquote>\n<p>I learned something new here. Empirically, it seems the tile-based approach hurt private LB performance.\nDo you have any tips on how we could have noticed or expected this before the private scores came out? In my experiments, CV and public LB were pretty well correlated.</p>",
          "votes": null,
          "replies": [
            {
              "id": 3398948,
              "author_name": "quan0095",
              "author_url": "",
              "post_date": "01/30/2026 00:31:44",
              "content": "<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2832287%2F30633138e205f82348fb403dabbb2ead%2FScreenshot%202026-01-30%20at%2007.29.49.png?generation=1769733019317622&amp;alt=media\" alt=\"\">\nOh, for me, 8 tiles is much worse than 2 tiles on the private leaderboard, even though the public scores are the same.</p>",
              "votes": null,
              "replies": [
                {
                  "id": 3398985,
                  "author_name": "yeoyunsianggeremie",
                  "author_url": "",
                  "post_date": "01/30/2026 02:29:19",
                  "content": "<p>I guess lots of the top teams on the public leaderboard used the same tile-level chunking method, many of them dropped by similar magnitude.</p>\n<p>I wonder is it because overprediction of clovers?</p>",
                  "votes": null,
                  "replies": []
                }
              ]
            },
            {
              "id": 3399013,
              "author_name": "dangnh0611",
              "author_url": "",
              "post_date": "01/30/2026 03:56:45",
              "content": "<p>I did not employ any tiles-level feature aggregation, predict each biomass targets for each tile then sum all (hopefully prevent overfitting). But more tiles, CV decreases which I can't find way to recover the accuracy. I mean, global context is still important, model just looking at overall shape/semantic and guess, so DINOv3 <strong>VIT (but not ConvNeXT)</strong>  dominates this competition.\nHere are some local CV results on frozen backbone, did not submit to LB:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F10254700%2F52158752b421ab748f27ac6e6f8bd2b9%2FScreenshot%202026-01-30%20at%2010.52.03.png?generation=1769745330703345&amp;alt=media\" alt=\"\"></p>",
              "votes": null,
              "replies": []
            },
            {
              "id": 3399020,
              "author_name": "quan0095",
              "author_url": "",
              "post_date": "01/30/2026 04:13:04",
              "content": "<p>Yeah, when I submitted the 8-tile solution to the public leaderboard, the score dropped a bit, which is why I didn’t choose the 8-tile solution as my main approach. I used the 2-tile solution as the primary one and added the 8-tile solution into the ensemble, but it actually dragged my ensemble score down 😅</p>",
              "votes": null,
              "replies": [
                {
                  "id": 3399274,
                  "author_name": "gauravbrills",
                  "author_url": "",
                  "post_date": "01/30/2026 14:44:36",
                  "content": "<p>Ahh was it really tiles ,, we are testing with other folds if it was folds or tiles :( . </p>",
                  "votes": null,
                  "replies": []
                }
              ]
            },
            {
              "id": 3399297,
              "author_name": "forgakki",
              "author_url": "",
              "post_date": "01/30/2026 15:49:30",
              "content": "<p>In my experiments, 32 tile-base sum apporach improve my lb and cv significantly,  but I think this method lost the global feature. I try many method to fuse local and global information, but still can not get good result. My best PB single model use self-supervised learning and global feature to predict the local tile position, but get low LB.  I still feel that the dataset for this competition might be relatively small, resulting in a certain level of randomness in the outcomes. Moreover, some biomass information cannot be determined solely from images.\nSo, your method might actually be more effective with larger datasets.</p>",
              "votes": null,
              "replies": []
            }
          ]
        }
      ]
    },
    {
      "id": 3399031,
      "author_name": "pcjimmmy",
      "author_url": "",
      "post_date": "01/30/2026 04:46:37",
      "content": "<p>Sorry to see you guys took such a big drop.  Lots of things to learn about small sample size model building.  Have not looked at the results the hosts got with meta only data a decade ago, but seems like vision did not add as much as they might have hoped.</p>\n<p>The images were pretty crappy - hopefully if you do a vision model you don't generate low quality images and hope they \"generalize\" well.  The description of how the weights of each of the labels were done also seemed pretty sketchy to me.  Probably involved in several thousand experiments over my working years and I made the comment at the start of this competition that it was a pretty poor dataset.  </p>\n<p>Regardless did learn a lot myself about modeling with such a small number of images and had fun most of the 3 months.  Ended up spending almost $3000 to upgrade my computes, so really committed to getting an ROI over the rest of this year :)</p>",
      "votes": null,
      "replies": []
    },
    {
      "id": 3399310,
      "author_name": "pushpakbhoge",
      "author_url": "",
      "post_date": "01/30/2026 16:02:33",
      "content": "<p>I also tried to do something like this with 8 patches. I got it upto 0.6,1 but I did not understand ViT in its technical depth yet, so I gave up on this early and moved to a simpler structure. </p>\n<p>I basically tried to create a first transformer structure, which was pretrained and frozen. Then created another transformer layer, which treated the first transformer patch output as input patches and predicted the final feats to which I attached the heads.</p>\n<pre><code>class BioMassViTRegressor(nn.Module):\n    def __init__(\n        self,\n        backbone: str,\n        weights: Optional[Any] = None,\n        num_targets: int = 4,\n        drop_rate: float = 0.3,\n        patch_w: int = 4,\n        patch_h: int = 2,\n        cp: bool = True,\n    ):\n        super().__init__()\n\n        # save the attributes\n        self.num_targets = num_targets\n        self.num_patches = patch_w * patch_h\n        self.cp = cp\n\n        # build initial patch creator for processing high res image\n        self.patchifier = Rearrange(\"n c (h ph) (w pw) -&gt; n (h w) c ph pw\", h=patch_h, w=patch_w)\n\n        # build backbone layers\n        backbone, out_dim = self.build_backbone(backbone, weights=weights)\n        self.backbone = backbone\n\n        # build local patch compressor (Attentive Pooling)\n        self.compressor = PatchCompressor(out_dim)\n\n        # global transformer aggregator (~1.5M params)\n        self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, out_dim))\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=out_dim, nhead=self.num_patches, dim_feedforward=out_dim * 2, dropout=drop_rate, batch_first=True\n        )\n        self.global_transformer = nn.TransformerEncoder(encoder_layer, num_layers=1)\n\n        # 5. Final Regression Head\n        self.head = nn.Sequential(\n            nn.LayerNorm(out_dim),\n            nn.Linear(out_dim, out_dim // 2),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(out_dim // 2, num_targets),\n        )\n\n    def freeze_backbone(self, stem: bool = True, stages: int = 4):\n        # unfreeze everything first\n        for param in self.backbone.parameters():\n            param.requires_grad = True\n\n        # apply stem freeze if requested\n        if stem:\n            for param in self.backbone.patch_embed.parameters():\n                param.requires_grad = False\n\n        # apply stage-wise freeze\n        if stages != 0:\n            for stage_idx in range(stages):\n                block_start_idx = stage_idx * 3\n                for block_idx in range(block_start_idx, block_start_idx + 3):\n                    for param in self.backbone.blocks[block_idx].parameters():\n                        param.requires_grad = False\n\n    def build_backbone(self, backbone: str, weights: Optional[str], **kwargs):\n        if backbone.startswith(\"vit\"):\n            if weights is None:\n                model = timm.create_model(backbone, pretrained=False, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == False:\n                model = timm.create_model(f\"{backbone}.{weights}\", pretrained=True, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == True:\n                model = timm.create_model(backbone, pretrained=None, checkpoint_path=weights, num_classes=0, **kwargs)\n\n            # mask the head and return the backbone\n            model.head = nn.Identity()\n            return model, model.head_hidden_size\n\n        else:\n            raise ValueError(\"Unknown backbone requested\")\n\n    def forward(self, x: torch.Tensor) -&gt; torch.Tensor:\n        batch_size = x.shape[0]\n\n        # create patches and extract patch features\n        patch_feats = []\n        if len(x.shape) == 4:\n            x = self.patchifier(x)\n        for patch_idx in range(self.num_patches):\n            feat_patch = checkpoint(self.backbone.forward_features, x[:, patch_idx, :, :], use_reentrant=False)\n            patch_feats.append(feat_patch)\n\n        # stitching individual patches in one\n        x = patch_feats\n        x = torch.stack(patch_feats, dim=1)\n        x = x.view(-1, x.shape[2], x.shape[3])\n        x = self.compressor(x)\n        x = x.view(batch_size, self.num_patches, -1)\n        x = x + self.pos_embed\n        x = self.global_transformer(x)\n        x = x.mean(dim=1)\n        return self.head(x)\n</code></pre>",
      "votes": null,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "3398702": "# TL;DR\nOur approach reconstructs the input image into a 32-tile grid and learns the global representation using ViT + 2D positional embeddings + Multi-head MIL attention pooling. Instead of predicting all 5 targets directly, we predict the 3 main targets (`DG`, `GDM`, `DT`) and physically reconstruct the remaining two (`DD`, `DC`) to align with the metric. We also utilized LUPI (Learning Using Privileged Information) via a meta-teacher to distill knowledge from train-only metadata into the vision model, enabling image-only inference.\n\n\n![Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F13143896%2F3faf64ba4b9134cb37becc3432355853%2Fimage%20(2).png?generation=1769698780181835&alt=media)\n**Figure 1. Overview (Inference): Tile-based ViT with 2D positional embeddings and MIL pooling**\n\n## 1. Data preprocessing\n\n### Features\n\n- **Meta**: `Pre_GSHH_NDVI`, `Height_Ave_cm`, `Sampling_Date`, `State`, `Species`\n- **Derived**\n    - `Month`: extracted from `Sampling_Date`\n    - `State_encoded`, `Species_encoded`: label encoding\n\n### Quality meta (train-time meta feature)\n(Ref. https://www.kaggle.com/competitions/csiro-biomass/discussion/667537)\n\n- `q_sharpness`: Laplacian variance (sharpness)\n- `q_noise`: residual mean after Gaussian blur (noise proxy)\n- `q_device_cluster`: `StandardScaler + KMeans(n_clusters=2)`\n\n### 32-tile input construction (4×8 global grid)\n- Original image: **2000×1000**\n- Crops (each 1000×1000)\n    - Left: x 0–1000\n    - Middle: x 500–1500\n    - Right: x 1000–2000\n- Build 32 tiles of 250×250 on a global 4×8 grid:\n    - global col 0..1: from **Left** (local col 0..1) → 8 tiles\n    - global col 2..5: from **Middle** (local col 0..3) → 16 tiles\n    - global col 6..7: from **Right** (local col 2..3) → 8 tiles\n        → total 32 tiles (row-major order)\n\n### Augmentation / transform\n- Crop-level aug: random H/V flip, ColorJitter\n- Per-tile transform: Resize(**img_size=448**), Normalize\n- Tile dropout: Dropout for some tiles\n\n## 2. Data split strategy\n\n### StratifiedGroupKFold\n- **Group key**: `Sampling_Date + State`\n- **Stratify**: After quantile binning `Dry_Total_g` using `qcut(n_bins=5)`, stratify\n\nWe used 4 folds, and the cross-validation was stable between folds.\n\n## 3. Method\n### 3.1 Backbone + tile feature extraction\n- Backbone: `timm/vit_huge_plus_patch16_dinov3...` (`num_classes=0`, `global_pool=avg`)\n- Input: `(B, 32, C, H, W)` → reshape `(B*32, C, H, W)` → backbone → `(B, 32, F)`\n\n### 3.2 2D positional embedding + multi-head MIL attention pooling\n- Global spatial information is injected by adding 2D positional embeddings `(row_emb + col_emb) `to the tile features.\n- The attention scorer computes softmax weights over the tile dimension (per head) and aggregates the weighted features.\n- An output projection maps the result back to `(B, F)`, keeping the downstream heads simple.\n\n### 3.3 Heads: main(3) + aux(DD/DC)\n\n- **Main head**: Predicts the three primary targets `(DG, GDM, DT)`.\n- **Aux heads**: Independently predict `DD` and `DC` (with ReLU to prevent negative values).\n   - Not used during inference by default.\n\n### 3.4 Postprocess + 5-target reconstruction\n\n- Enforce constraints:\n    - `DG ≥ 0`, `GDM ≥ DG`, `DT ≥ GDM`\n- 5-target reconstruction:\n    - `DD = DT - GDM`\n    - `DC = GDM - DG`\n\n### 3.5 Loss\n\n- **Base**: `SmoothL1Loss` applied to the 3 main targets.\n- **R² Surrogate Loss**:\n   - Normalized MSE based on SST calculated from the train split.\n   - Importance weights: [0.1, 0.2, 1.0] for `(DG, GDM, DT)`.\n- **Hybrid**:\n   - `loss = alpha * SmoothL1 + (1-alpha) * R²_surrogate` (with `alpha=0.5`).\n   - Empirically, `SmoothL1` contributed to better training stability.\n\n\n### 3.6 Aux + consistency + log loss\n\n- **Aux**: SmoothL1 on DD/DC.\n- **Log-space L1**: Improves stability for large-scale targets.\n- **Consistency Loss** (`consis_w=0.2`): Enforces alignment between aux predictions and reconstructed values:\n   -  Induce `DD_aux_pred` to match `(DT−GDM)` and `DC_aux_pred` to match `(GDM−DG)`.\n\n### 3.7 Train-time meta teacher (LUPI) — knowledge distillation\n\n- **Meta input**: NDVI, Height, State/Species, Month + quality meta(sharpness/noise/cluster)\n- **Teacher predicts ratios (sigmoid)**:\n   - `DG/DT`, `GDM/DT`, `DD/DT`.\n   - `DC/DT` is excluded as redundant (derivable from `GDM/DT − DG/DT`).\n- **Distillation**: Teacher outputs are supervised by target ratios, while student ratios are distilled from the teacher's predictions.\n- **Absolute DD**: We also separately supervise and distill the absolute DD value (`teacher_dd`, `out_dim=1`).\n\n### 3.8 Two-stage fine-tuning\n\n- **Stage 1**: Unfreeze last 1 block (`lr=1e-3`) to stably train the head, MIL, and meta components.\n- **Stage 2**: Unfreeze last 3 blocks (`lr=1e-4`) to fine-tune and push the performance ceiling.\n\n## 4. Results\n- **Best Single Model:**\n    - **CV (SGKF 4-fold)**: **0.7615**\n    - **Public/Private LB**: **0.78/0.62**\n\n### Table: Single-model milestone progression (Baseline → Best)\n\n| Step | Tiles / img_size | Key change (what improved) | Mean CV<br>(4-fold) | Full OOF | Public LB | Δ Full OOF vs Reference† |\n| :--- | :--- | :--- | :--- | :--- | :--- | :--- |\n| Reference† | – | early Lightning baseline | 0.5802 | 0.6357 | 0.64 | +0.0000 (**+0.0%**) |\n| Baseline (comparable) | 12 / 512 | TileAttentionPool | 0.6972 | 0.7044 | 0.76 | +0.0687 (**+10.8%**) |\n| + Pooling | 12 / 512 | Multi-head MIL pooling | 0.7165 | 0.7328 | 0.76 | +0.0971 (**+15.3%**) |\n| + Tiles / scale | 32 / 256 | 32 tiles | 0.7224 | 0.7306 | 0.77 | +0.1144 (**+18.0%**) |\n| + Quality meta | 32 / 256 | + quality meta (q_sharpness/q_noise/cluster) | 0.7369 | 0.7509 | 0.77 | +0.1152 (**+18.1%**) |\n| **Best single** | **32 / 448** | **2D pos + MH-MIL + quality meta + R² loss** | **0.7468** | **0.7615** | **0.78** | **+0.1258 (+19.8%)** |\n\n*Mean CV and Full OOF are based on the same 4-fold pipeline. Δ(%) indicates the improvement in Full OOF compared to the Reference†.*\n\n## 5. Discussion & Retrospective\nWhile our solution achieved 6th place on the Public LB, we experienced a significant shake-up, finishing 129th on the Private LB. Below is a summary of our approach's strengths and our retrospective analysis on the drop.\n\n### What Worked\n- **2D Positional Embedding + MIL**: Explicitly modeling the spatial relationship of tiles with 2D embeddings significantly outperformed simple averaging or concatenation (LB 0.71 → 0.75).\n- **Target Reconstruction Strategy**: Focusing on the three highly correlated \"green biomass\" targets (`DG`, `GDM`, `DT`) and reconstructing the targets (`DD`, `DC`) proved effective for ensuring physical constraints (DG <= GDM <= DT) and stabilizing convergence.\n- **Metric-aligned Training**: Mixing the R² surrogate loss helped align the optimization direction with the competition metric.\n\n### Retrospective\nThe Shake-up (Public 6th → Private 129th) I suspect two main factors contributed to the lack of generalization on the Private set:\n1. **Over-reliance on Meta-Teacher (LUPI)**: We utilized a Meta-Teacher to distill metadata (NDVI, Season, etc.) into the vision model. While this boosted CV and Public LB, it likely introduced a **bias towards the training/public set's metadata distribution**. As noted in our risks, if the metadata distribution shifted significantly in the Private set, the \"internalized priors\" from the teacher might have harmed the model's robustness.\n2. **CV Strategy vs. Distribution Shift**: We used StratifiedGroupKFold (grouped by Date+State, stratified by Target bins) to maintain a stable CV-LB correlation. While this strategy successfully prevented leakage from same-day samples, it may not have been strict enough to simulate the domain shift present in the Private test set.\n\n## 6. Conclusion\nOur solution demonstrates that combining **2D positional embeddings** with **MIL pooling** is effective for high-resolution biomass estimation. However, the significant shake-up highlights the risk of **over-fitting to metadata priors via LUPI**. While distilling privileged information boosts performance on similar distributions, it requires more rigorous validation to ensure robustness against domain shifts in unseen data.",
    "3398817": "```\n32-tile input construction (4×8 global grid)\n```\n\nFeels like this splitting the image into independent tiles was the reason for the shake down. We observed the same. The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58. 500/500 250/250 100/100 or even 16/16 patch token level were all bad (with more tiles, the public and private moved in opposite directions)\n\nThe single image which performed trash on both CV and public LB did better on private. Which I felt is quite surprising since I thought 2000x1000 -> 448x448 had significant information loss 😐\n\nEdit: the 0.68 wasn’t even with Dino so it’s insane 😂",
    "3398939": "Thanks for the great insights.\n\n> The public went from 0.68 to 0.77 but private fell from 0.61 to 0.58.\n\nI learned something new here. Empirically, it seems the tile-based approach hurt private LB performance.\nDo you have any tips on how we could have noticed or expected this before the private scores came out? In my experiments, CV and public LB were pretty well correlated.",
    "3398948": "![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F2832287%2F30633138e205f82348fb403dabbb2ead%2FScreenshot%202026-01-30%20at%2007.29.49.png?generation=1769733019317622&alt=media)\nOh, for me, 8 tiles is much worse than 2 tiles on the private leaderboard, even though the public scores are the same.",
    "3398985": "I guess lots of the top teams on the public leaderboard used the same tile-level chunking method, many of them dropped by similar magnitude.\n\nI wonder is it because overprediction of clovers?",
    "3399013": "I did not employ any tiles-level feature aggregation, predict each biomass targets for each tile then sum all (hopefully prevent overfitting). But more tiles, CV decreases which I can't find way to recover the accuracy. I mean, global context is still important, model just looking at overall shape/semantic and guess, so DINOv3 **VIT (but not ConvNeXT)**  dominates this competition.\nHere are some local CV results on frozen backbone, did not submit to LB:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F10254700%2F52158752b421ab748f27ac6e6f8bd2b9%2FScreenshot%202026-01-30%20at%2010.52.03.png?generation=1769745330703345&alt=media)",
    "3399020": "Yeah, when I submitted the 8-tile solution to the public leaderboard, the score dropped a bit, which is why I didn’t choose the 8-tile solution as my main approach. I used the 2-tile solution as the primary one and added the 8-tile solution into the ensemble, but it actually dragged my ensemble score down 😅",
    "3399031": "Sorry to see you guys took such a big drop.  Lots of things to learn about small sample size model building.  Have not looked at the results the hosts got with meta only data a decade ago, but seems like vision did not add as much as they might have hoped.\n\nThe images were pretty crappy - hopefully if you do a vision model you don't generate low quality images and hope they \"generalize\" well.  The description of how the weights of each of the labels were done also seemed pretty sketchy to me.  Probably involved in several thousand experiments over my working years and I made the comment at the start of this competition that it was a pretty poor dataset.  \n\nRegardless did learn a lot myself about modeling with such a small number of images and had fun most of the 3 months.  Ended up spending almost $3000 to upgrade my computes, so really committed to getting an ROI over the rest of this year :)",
    "3399274": "Ahh was it really tiles ,, we are testing with other folds if it was folds or tiles :( .",
    "3399297": "In my experiments, 32 tile-base sum apporach improve my lb and cv significantly,  but I think this method lost the global feature. I try many method to fuse local and global information, but still can not get good result. My best PB single model use self-supervised learning and global feature to predict the local tile position, but get low LB.  I still feel that the dataset for this competition might be relatively small, resulting in a certain level of randomness in the outcomes. Moreover, some biomass information cannot be determined solely from images.\nSo, your method might actually be more effective with larger datasets.",
    "3399310": "I also tried to do something like this with 8 patches. I got it upto 0.6,1 but I did not understand ViT in its technical depth yet, so I gave up on this early and moved to a simpler structure. \n\nI basically tried to create a first transformer structure, which was pretrained and frozen. Then created another transformer layer, which treated the first transformer patch output as input patches and predicted the final feats to which I attached the heads.\n\n```\nclass BioMassViTRegressor(nn.Module):\n    def __init__(\n        self,\n        backbone: str,\n        weights: Optional[Any] = None,\n        num_targets: int = 4,\n        drop_rate: float = 0.3,\n        patch_w: int = 4,\n        patch_h: int = 2,\n        cp: bool = True,\n    ):\n        super().__init__()\n\n        # save the attributes\n        self.num_targets = num_targets\n        self.num_patches = patch_w * patch_h\n        self.cp = cp\n\n        # build initial patch creator for processing high res image\n        self.patchifier = Rearrange(\"n c (h ph) (w pw) -> n (h w) c ph pw\", h=patch_h, w=patch_w)\n\n        # build backbone layers\n        backbone, out_dim = self.build_backbone(backbone, weights=weights)\n        self.backbone = backbone\n\n        # build local patch compressor (Attentive Pooling)\n        self.compressor = PatchCompressor(out_dim)\n\n        # global transformer aggregator (~1.5M params)\n        self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, out_dim))\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=out_dim, nhead=self.num_patches, dim_feedforward=out_dim * 2, dropout=drop_rate, batch_first=True\n        )\n        self.global_transformer = nn.TransformerEncoder(encoder_layer, num_layers=1)\n\n        # 5. Final Regression Head\n        self.head = nn.Sequential(\n            nn.LayerNorm(out_dim),\n            nn.Linear(out_dim, out_dim // 2),\n            nn.ReLU(),\n            nn.Dropout(drop_rate),\n            nn.Linear(out_dim // 2, num_targets),\n        )\n\n    def freeze_backbone(self, stem: bool = True, stages: int = 4):\n        # unfreeze everything first\n        for param in self.backbone.parameters():\n            param.requires_grad = True\n\n        # apply stem freeze if requested\n        if stem:\n            for param in self.backbone.patch_embed.parameters():\n                param.requires_grad = False\n\n        # apply stage-wise freeze\n        if stages != 0:\n            for stage_idx in range(stages):\n                block_start_idx = stage_idx * 3\n                for block_idx in range(block_start_idx, block_start_idx + 3):\n                    for param in self.backbone.blocks[block_idx].parameters():\n                        param.requires_grad = False\n\n    def build_backbone(self, backbone: str, weights: Optional[str], **kwargs):\n        if backbone.startswith(\"vit\"):\n            if weights is None:\n                model = timm.create_model(backbone, pretrained=False, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == False:\n                model = timm.create_model(f\"{backbone}.{weights}\", pretrained=True, num_classes=0, **kwargs)\n            elif isinstance(weights, str) and weights.startswith(\"./\") == True:\n                model = timm.create_model(backbone, pretrained=None, checkpoint_path=weights, num_classes=0, **kwargs)\n\n            # mask the head and return the backbone\n            model.head = nn.Identity()\n            return model, model.head_hidden_size\n\n        else:\n            raise ValueError(\"Unknown backbone requested\")\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        batch_size = x.shape[0]\n\n        # create patches and extract patch features\n        patch_feats = []\n        if len(x.shape) == 4:\n            x = self.patchifier(x)\n        for patch_idx in range(self.num_patches):\n            feat_patch = checkpoint(self.backbone.forward_features, x[:, patch_idx, :, :], use_reentrant=False)\n            patch_feats.append(feat_patch)\n\n        # stitching individual patches in one\n        x = patch_feats\n        x = torch.stack(patch_feats, dim=1)\n        x = x.view(-1, x.shape[2], x.shape[3])\n        x = self.compressor(x)\n        x = x.view(batch_size, self.num_patches, -1)\n        x = x + self.pos_embed\n        x = self.global_transformer(x)\n        x = x.mean(dim=1)\n        return self.head(x)\n```"
  },
  "source": "meta"
}