{
  "id": 670653,
  "title": "CSIRO 2026 Rank 25th Solution",
  "url": "/competitions/csiro-biomass/writeups/rank-28-solution",
  "author_name": "",
  "post_date": "2026-01-29T01:30:40.590Z",
  "votes": 13,
  "comment_count": 4,
  "views": 0,
  "content": "<p><strong>Author:</strong> Zulqarnain Ali </p>\n<p><strong>Competition:</strong> CSIRO - Image2Biomass Prediction  </p>\n<h2><strong>Final Ranking:</strong> Private LB 26th / Public LB 105th  </h2>\n<h2>My Approach at a Glance</h2>\n<p>Like many other i too used DINO .I built a dual-pathway system that combines two very different approaches:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9ecb63fd1c14cf98aced2be51cb846c2%2Fmermaid-diagram-2026-01-29-061928.png?generation=1769649584396841&amp;alt=media\" alt=\"\"></p>\n<p>The main thing  was that DINO v3 and SigLIP learn completely different things from images. DINO excels at visual patterns (what green looks like versus what dead grass looks like), while SigLIP understands semantic concepts. Combining them gave me better results than either alone.</p>\n<h2>Dataset</h2>\n<p>Based on the competition paper, I learned that this dataset is actually pretty special in the agricultural AI world:</p>\n<ul>\n<li><strong>1,162 images</strong> that passed strict quality control (from an initial 3,187 collected)</li>\n<li><strong>19 sites</strong> across NSW, Victoria, Tasmania, and Western Australia</li>\n<li><strong>Multiple seasons</strong> from 2014-2017</li>\n<li><strong>Multiple camera types</strong> including consumer devices like iPhone 4/5s, which makes the data more realistic</li>\n</ul>\n<p>The paper mentions that NDVI values were collected using a GreenSeeker handheld sensor held about 1 meter above the quadrat, with 100 readings averaged. They also measured pasture height with a falling plate meter. These weren't available at test time, so I focused purely on visual features.</p>\n<p>For the targets, I paid special attention to the relationships:</p>\n<ul>\n<li><strong>GDM_g = Green + Clover</strong> (Green Dry Matter - the digestible stuff)</li>\n<li><strong>Total = GDM + Dead</strong> (everything available to eat)</li>\n</ul>\n<p>This meant my predictions needed to maintain these relationships, or the post-processing would fix them.</p>\n<h2>Model Architecture</h2>\n<h3>The DINO v3 Backbone</h3>\n<p>I used DINO v3 huge (<code>vit_huge_plus_patch16_dinov3.lvd1689m</code>) as my backbone. The image splitting strategy was crucial - I split each 2000x1000 image vertically in half, processed each half separately through the same backbone, then concatenated the features.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fa500c213db078ac0d37ba3fddcf85618%2Fmermaid-diagram-2026-01-29-062822.png?generation=1769650120965240&amp;alt=media\" alt=\"\">\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9a25dd4054446dfd3322d25de8506e17%2Fmermaid-diagram-2026-01-29-062911.png?generation=1769650166008030&amp;alt=media\" alt=\"\"></p>\n<h3>The Local Mamba Block</h3>\n<p>For feature fusion, I experimented with several options and found that Local Mamba blocks worked well. This is a lightweight attention-like mechanism:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F5c4384db3e57fa5fec12ebe284c2adf3%2Fmermaid-diagram-2026-01-29-060909.png?generation=1769648968305823&amp;alt=media\" alt=\"\"></p>\n<p>The depthwise convolution captures local patterns efficiently, and the gating mechanism controls information flow while maintaining gradient flow through the residual connection.</p>\n<h3>SigLIP Semantic Features</h3>\n<p>The second pathway uses SigLIP SO400M for semantic understanding. I extracted embeddings and computed similarity scores against agricultural concepts:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F21855fb7016bbc3db22d42c72728b9c5%2Fmermaid-diagram-2026-01-29-061033.png?generation=1769649044772177&amp;alt=media\" alt=\"\"></p>\n<p>The concept-based scoring helps the model understand pasture composition in semantic terms, not just raw pixels.</p>\n<hr>\n<h2>Training Strategy</h2>\n<h3>Cross-Validation</h3>\n<p>I used Stratified Group K-Fold with 3 folds, stratifying on <code>Dry_Total_g</code> quintiles and grouping by image ID to prevent data leakage:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F1faac336a6241dcd4d475d1298eca6d1%2Fmermaid-diagram-2026-01-29-061154.png?generation=1769649126810187&amp;alt=media\" alt=\"\"></p>\n<h3>Training Config</h3>\n<p>Here's what worked for me:</p>\n<table>\n<thead>\n<tr>\n<th>Parameter</th>\n<th>Value</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Image Size</td>\n<td>768x384</td>\n<td>Balance detail vs memory</td>\n</tr>\n<tr>\n<td>Batch Size</td>\n<td>4</td>\n<td>GPU memory limit</td>\n</tr>\n<tr>\n<td>Epochs</td>\n<td>180</td>\n<td>Full training run</td>\n</tr>\n<tr>\n<td>LR Backbone</td>\n<td>1e-5</td>\n<td>Conservative for pretrained</td>\n</tr>\n<tr>\n<td>LR Head</td>\n<td>5e-4</td>\n<td>Higher for new heads</td>\n</tr>\n<tr>\n<td>Dropout</td>\n<td>0.2</td>\n<td>Prevent overfitting</td>\n</tr>\n<tr>\n<td>Early Stop</td>\n<td>30 epochs</td>\n<td>Regularization</td>\n</tr>\n</tbody>\n</table>\n<h3>Data Augmentation</h3>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fcd34a9cd81a53ba7462a7f18a5384ac3%2Fmermaid-diagram-2026-01-29-061300.png?generation=1769649193655864&amp;alt=media\" alt=\"\"></p>\n<h3>Loss Function</h3>\n<p>I designed a custom loss that matches the competition metric:</p>\n<pre><code>class WeightedBiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n        self.huber = nn.SmoothL1Loss(reduction='none', beta=5.0)\n\n    def forward(self, preds, labels):\n        preds_log = torch.log1p(preds)\n        labels_log = torch.log1p(labels)\n        loss = self.huber(preds_log, labels_log)\n        weighted_loss = (loss * self.weights.to(loss.device)).mean()\n        return weighted_loss\n</code></pre>\n<p>The log transform stabilizes the wide range of biomass values, and Huber loss handles outliers gracefully.</p>\n<hr>\n<h2>Inference Pipeline</h2>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F2d57241ac810573ab579e2b39c706c73%2Fmermaid-diagram-2026-01-29-061411.png?generation=1769649272533056&amp;alt=media\" alt=\"\"></p>\n<p>Post-processing included:</p>\n<ul>\n<li>Clipping all predictions to non-negative values</li>\n<li>Recalculating GDM_g and Total_g from components</li>\n<li>Applying small adjustments to Dead based on its predicted range</li>\n</ul>\n<hr>\n<h2>Results</h2>\n<h3>Cross-Validation Scores</h3>\n<table>\n<thead>\n<tr>\n<th>Fold</th>\n<th>CV R²</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>0.838</td>\n</tr>\n<tr>\n<td>1</td>\n<td>0.871</td>\n</tr>\n<tr>\n<td>2</td>\n<td>0.822</td>\n</tr>\n<tr>\n<td><strong>Mean</strong></td>\n<td><strong>0.844 ± 0.021</strong></td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>What I Learned</h2>\n<ol>\n<li><p><strong>Image splitting helps</strong> - Processing left and right halves separately gave the model more spatial flexibility than trying to learn from the whole image at once.</p></li>\n<li><p><strong>DINO + SigLIP is a powerful combo</strong> - DINO's visual patterns + SigLIP's semantic understanding captured different aspects of pasture composition.</p></li>\n<li><p><strong>Respect the target relationships</strong> - GDM and Total being derived meant I had to ensure my predictions maintained these relationships or fix them in post-processing.</p></li>\n<li><p><strong>Regularization was essential</strong> - With ~1000 training images, dropout, weight decay, and early stopping prevented overfitting.</p></li>\n<li><p><strong>The paper matters</strong> - Understanding the data collection process helped me design better preprocessing and augmentation strategies.</p></li>\n</ol>\n<h2>The complete code can be found here : <a href=\"https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction\" target=\"_blank\">https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction</a></h2>",
  "messages": [
    {
      "id": "3398326",
      "postDate": "01/29/2026 01:29:58",
      "content": "<p><strong>Author:</strong> Zulqarnain Ali </p>\n<p><strong>Competition:</strong> CSIRO - Image2Biomass Prediction  </p>\n<h2><strong>Final Ranking:</strong> Private LB 26th / Public LB 105th  </h2>\n<h2>My Approach at a Glance</h2>\n<p>Like many other i too used DINO .I built a dual-pathway system that combines two very different approaches:\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9ecb63fd1c14cf98aced2be51cb846c2%2Fmermaid-diagram-2026-01-29-061928.png?generation=1769649584396841&amp;alt=media\" alt=\"\"></p>\n<p>The main thing  was that DINO v3 and SigLIP learn completely different things from images. DINO excels at visual patterns (what green looks like versus what dead grass looks like), while SigLIP understands semantic concepts. Combining them gave me better results than either alone.</p>\n<h2>Dataset</h2>\n<p>Based on the competition paper, I learned that this dataset is actually pretty special in the agricultural AI world:</p>\n<ul>\n<li><strong>1,162 images</strong> that passed strict quality control (from an initial 3,187 collected)</li>\n<li><strong>19 sites</strong> across NSW, Victoria, Tasmania, and Western Australia</li>\n<li><strong>Multiple seasons</strong> from 2014-2017</li>\n<li><strong>Multiple camera types</strong> including consumer devices like iPhone 4/5s, which makes the data more realistic</li>\n</ul>\n<p>The paper mentions that NDVI values were collected using a GreenSeeker handheld sensor held about 1 meter above the quadrat, with 100 readings averaged. They also measured pasture height with a falling plate meter. These weren't available at test time, so I focused purely on visual features.</p>\n<p>For the targets, I paid special attention to the relationships:</p>\n<ul>\n<li><strong>GDM_g = Green + Clover</strong> (Green Dry Matter - the digestible stuff)</li>\n<li><strong>Total = GDM + Dead</strong> (everything available to eat)</li>\n</ul>\n<p>This meant my predictions needed to maintain these relationships, or the post-processing would fix them.</p>\n<h2>Model Architecture</h2>\n<h3>The DINO v3 Backbone</h3>\n<p>I used DINO v3 huge (<code>vit_huge_plus_patch16_dinov3.lvd1689m</code>) as my backbone. The image splitting strategy was crucial - I split each 2000x1000 image vertically in half, processed each half separately through the same backbone, then concatenated the features.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fa500c213db078ac0d37ba3fddcf85618%2Fmermaid-diagram-2026-01-29-062822.png?generation=1769650120965240&amp;alt=media\" alt=\"\">\n<img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9a25dd4054446dfd3322d25de8506e17%2Fmermaid-diagram-2026-01-29-062911.png?generation=1769650166008030&amp;alt=media\" alt=\"\"></p>\n<h3>The Local Mamba Block</h3>\n<p>For feature fusion, I experimented with several options and found that Local Mamba blocks worked well. This is a lightweight attention-like mechanism:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F5c4384db3e57fa5fec12ebe284c2adf3%2Fmermaid-diagram-2026-01-29-060909.png?generation=1769648968305823&amp;alt=media\" alt=\"\"></p>\n<p>The depthwise convolution captures local patterns efficiently, and the gating mechanism controls information flow while maintaining gradient flow through the residual connection.</p>\n<h3>SigLIP Semantic Features</h3>\n<p>The second pathway uses SigLIP SO400M for semantic understanding. I extracted embeddings and computed similarity scores against agricultural concepts:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F21855fb7016bbc3db22d42c72728b9c5%2Fmermaid-diagram-2026-01-29-061033.png?generation=1769649044772177&amp;alt=media\" alt=\"\"></p>\n<p>The concept-based scoring helps the model understand pasture composition in semantic terms, not just raw pixels.</p>\n<hr>\n<h2>Training Strategy</h2>\n<h3>Cross-Validation</h3>\n<p>I used Stratified Group K-Fold with 3 folds, stratifying on <code>Dry_Total_g</code> quintiles and grouping by image ID to prevent data leakage:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F1faac336a6241dcd4d475d1298eca6d1%2Fmermaid-diagram-2026-01-29-061154.png?generation=1769649126810187&amp;alt=media\" alt=\"\"></p>\n<h3>Training Config</h3>\n<p>Here's what worked for me:</p>\n<table>\n<thead>\n<tr>\n<th>Parameter</th>\n<th>Value</th>\n<th>Why</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Image Size</td>\n<td>768x384</td>\n<td>Balance detail vs memory</td>\n</tr>\n<tr>\n<td>Batch Size</td>\n<td>4</td>\n<td>GPU memory limit</td>\n</tr>\n<tr>\n<td>Epochs</td>\n<td>180</td>\n<td>Full training run</td>\n</tr>\n<tr>\n<td>LR Backbone</td>\n<td>1e-5</td>\n<td>Conservative for pretrained</td>\n</tr>\n<tr>\n<td>LR Head</td>\n<td>5e-4</td>\n<td>Higher for new heads</td>\n</tr>\n<tr>\n<td>Dropout</td>\n<td>0.2</td>\n<td>Prevent overfitting</td>\n</tr>\n<tr>\n<td>Early Stop</td>\n<td>30 epochs</td>\n<td>Regularization</td>\n</tr>\n</tbody>\n</table>\n<h3>Data Augmentation</h3>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fcd34a9cd81a53ba7462a7f18a5384ac3%2Fmermaid-diagram-2026-01-29-061300.png?generation=1769649193655864&amp;alt=media\" alt=\"\"></p>\n<h3>Loss Function</h3>\n<p>I designed a custom loss that matches the competition metric:</p>\n<pre><code>class WeightedBiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n        self.huber = nn.SmoothL1Loss(reduction='none', beta=5.0)\n\n    def forward(self, preds, labels):\n        preds_log = torch.log1p(preds)\n        labels_log = torch.log1p(labels)\n        loss = self.huber(preds_log, labels_log)\n        weighted_loss = (loss * self.weights.to(loss.device)).mean()\n        return weighted_loss\n</code></pre>\n<p>The log transform stabilizes the wide range of biomass values, and Huber loss handles outliers gracefully.</p>\n<hr>\n<h2>Inference Pipeline</h2>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F2d57241ac810573ab579e2b39c706c73%2Fmermaid-diagram-2026-01-29-061411.png?generation=1769649272533056&amp;alt=media\" alt=\"\"></p>\n<p>Post-processing included:</p>\n<ul>\n<li>Clipping all predictions to non-negative values</li>\n<li>Recalculating GDM_g and Total_g from components</li>\n<li>Applying small adjustments to Dead based on its predicted range</li>\n</ul>\n<hr>\n<h2>Results</h2>\n<h3>Cross-Validation Scores</h3>\n<table>\n<thead>\n<tr>\n<th>Fold</th>\n<th>CV R²</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>0</td>\n<td>0.838</td>\n</tr>\n<tr>\n<td>1</td>\n<td>0.871</td>\n</tr>\n<tr>\n<td>2</td>\n<td>0.822</td>\n</tr>\n<tr>\n<td><strong>Mean</strong></td>\n<td><strong>0.844 ± 0.021</strong></td>\n</tr>\n</tbody>\n</table>\n<hr>\n<h2>What I Learned</h2>\n<ol>\n<li><p><strong>Image splitting helps</strong> - Processing left and right halves separately gave the model more spatial flexibility than trying to learn from the whole image at once.</p></li>\n<li><p><strong>DINO + SigLIP is a powerful combo</strong> - DINO's visual patterns + SigLIP's semantic understanding captured different aspects of pasture composition.</p></li>\n<li><p><strong>Respect the target relationships</strong> - GDM and Total being derived meant I had to ensure my predictions maintained these relationships or fix them in post-processing.</p></li>\n<li><p><strong>Regularization was essential</strong> - With ~1000 training images, dropout, weight decay, and early stopping prevented overfitting.</p></li>\n<li><p><strong>The paper matters</strong> - Understanding the data collection process helped me design better preprocessing and augmentation strategies.</p></li>\n</ol>\n<h2>The complete code can be found here : <a href=\"https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction\" target=\"_blank\">https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction</a></h2>",
      "rawMarkdown": "**Author:** Zulqarnain Ali \n\n**Competition:** CSIRO - Image2Biomass Prediction  \n**Final Ranking:** Private LB 26th / Public LB 105th  \n---\n\n## My Approach at a Glance\n\nLike many other i too used DINO .I built a dual-pathway system that combines two very different approaches:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9ecb63fd1c14cf98aced2be51cb846c2%2Fmermaid-diagram-2026-01-29-061928.png?generation=1769649584396841&alt=media)\n\nThe main thing  was that DINO v3 and SigLIP learn completely different things from images. DINO excels at visual patterns (what green looks like versus what dead grass looks like), while SigLIP understands semantic concepts. Combining them gave me better results than either alone.\n\n\n## Dataset \n\nBased on the competition paper, I learned that this dataset is actually pretty special in the agricultural AI world:\n\n- **1,162 images** that passed strict quality control (from an initial 3,187 collected)\n- **19 sites** across NSW, Victoria, Tasmania, and Western Australia\n- **Multiple seasons** from 2014-2017\n- **Multiple camera types** including consumer devices like iPhone 4/5s, which makes the data more realistic\n\nThe paper mentions that NDVI values were collected using a GreenSeeker handheld sensor held about 1 meter above the quadrat, with 100 readings averaged. They also measured pasture height with a falling plate meter. These weren't available at test time, so I focused purely on visual features.\n\nFor the targets, I paid special attention to the relationships:\n- **GDM_g = Green + Clover** (Green Dry Matter - the digestible stuff)\n- **Total = GDM + Dead** (everything available to eat)\n\nThis meant my predictions needed to maintain these relationships, or the post-processing would fix them.\n\n\n## Model Architecture\n\n### The DINO v3 Backbone\n\nI used DINO v3 huge (`vit_huge_plus_patch16_dinov3.lvd1689m`) as my backbone. The image splitting strategy was crucial - I split each 2000x1000 image vertically in half, processed each half separately through the same backbone, then concatenated the features.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fa500c213db078ac0d37ba3fddcf85618%2Fmermaid-diagram-2026-01-29-062822.png?generation=1769650120965240&alt=media)\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9a25dd4054446dfd3322d25de8506e17%2Fmermaid-diagram-2026-01-29-062911.png?generation=1769650166008030&alt=media)\n\n### The Local Mamba Block\n\nFor feature fusion, I experimented with several options and found that Local Mamba blocks worked well. This is a lightweight attention-like mechanism:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F5c4384db3e57fa5fec12ebe284c2adf3%2Fmermaid-diagram-2026-01-29-060909.png?generation=1769648968305823&alt=media)\n\nThe depthwise convolution captures local patterns efficiently, and the gating mechanism controls information flow while maintaining gradient flow through the residual connection.\n\n### SigLIP Semantic Features\n\nThe second pathway uses SigLIP SO400M for semantic understanding. I extracted embeddings and computed similarity scores against agricultural concepts:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F21855fb7016bbc3db22d42c72728b9c5%2Fmermaid-diagram-2026-01-29-061033.png?generation=1769649044772177&alt=media)\n\nThe concept-based scoring helps the model understand pasture composition in semantic terms, not just raw pixels.\n\n---\n\n## Training Strategy\n\n### Cross-Validation\n\nI used Stratified Group K-Fold with 3 folds, stratifying on `Dry_Total_g` quintiles and grouping by image ID to prevent data leakage:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F1faac336a6241dcd4d475d1298eca6d1%2Fmermaid-diagram-2026-01-29-061154.png?generation=1769649126810187&alt=media)\n\n\n### Training Config\n\nHere's what worked for me:\n\n| Parameter | Value | Why |\n|-----------|-------|-----|\n| Image Size | 768x384 | Balance detail vs memory |\n| Batch Size | 4 | GPU memory limit |\n| Epochs | 180 | Full training run |\n| LR Backbone | 1e-5 | Conservative for pretrained |\n| LR Head | 5e-4 | Higher for new heads |\n| Dropout | 0.2 | Prevent overfitting |\n| Early Stop | 30 epochs | Regularization |\n\n### Data Augmentation\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fcd34a9cd81a53ba7462a7f18a5384ac3%2Fmermaid-diagram-2026-01-29-061300.png?generation=1769649193655864&alt=media)\n\n### Loss Function\n\nI designed a custom loss that matches the competition metric:\n\n```python\nclass WeightedBiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n        self.huber = nn.SmoothL1Loss(reduction='none', beta=5.0)\n\n    def forward(self, preds, labels):\n        preds_log = torch.log1p(preds)\n        labels_log = torch.log1p(labels)\n        loss = self.huber(preds_log, labels_log)\n        weighted_loss = (loss * self.weights.to(loss.device)).mean()\n        return weighted_loss\n```\n\nThe log transform stabilizes the wide range of biomass values, and Huber loss handles outliers gracefully.\n\n---\n\n## Inference Pipeline\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F2d57241ac810573ab579e2b39c706c73%2Fmermaid-diagram-2026-01-29-061411.png?generation=1769649272533056&alt=media)\n\nPost-processing included:\n- Clipping all predictions to non-negative values\n- Recalculating GDM_g and Total_g from components\n- Applying small adjustments to Dead based on its predicted range\n\n---\n\n## Results\n\n### Cross-Validation Scores\n\n| Fold | CV R² |\n|------|-------|\n| 0 | 0.838 |\n| 1 | 0.871 |\n| 2 | 0.822 |\n| **Mean** | **0.844 ± 0.021** |\n\n\n---\n\n## What I Learned\n\n1. **Image splitting helps** - Processing left and right halves separately gave the model more spatial flexibility than trying to learn from the whole image at once.\n\n2. **DINO + SigLIP is a powerful combo** - DINO's visual patterns + SigLIP's semantic understanding captured different aspects of pasture composition.\n\n3. **Respect the target relationships** - GDM and Total being derived meant I had to ensure my predictions maintained these relationships or fix them in post-processing.\n\n4. **Regularization was essential** - With ~1000 training images, dropout, weight decay, and early stopping prevented overfitting.\n\n5. **The paper matters** - Understanding the data collection process helped me design better preprocessing and augmentation strategies.\n\n## The complete code can be found here : https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction",
      "votes": null
    },
    {
      "id": "3398341",
      "postDate": "01/29/2026 02:03:39",
      "content": "<p>Congrats! The dual-pathway design combining DINO v3 and SigLIP-GBDT is excellently executed. Nice job!</p>",
      "rawMarkdown": "Congrats! The dual-pathway design combining DINO v3 and SigLIP-GBDT is excellently executed. Nice job!",
      "votes": null
    },
    {
      "id": "3398342",
      "postDate": "01/29/2026 02:06:40",
      "content": "<p>Thank you for your detailed solution description! </p>\n<p>Could you please give me your understanding to reshape 1000x1000 -&gt; 768x384? My teammate discovered such hard reshaping too, but for me it was some sort of magic trick without natural understanding “How it really works”</p>",
      "rawMarkdown": "Thank you for your detailed solution description! \n\nCould you please give me your understanding to reshape 1000x1000 -> 768x384? My teammate discovered such hard reshaping too, but for me it was some sort of magic trick without natural understanding “How it really works”",
      "votes": null
    },
    {
      "id": "3398560",
      "postDate": "01/29/2026 10:19:53",
      "content": "<p><a href=\"https://www.kaggle.com/arturkhodakovskiy\" target=\"_blank\">@arturkhodakovskiy</a>  Great question! I advise you must read the competition paper too for better understanding but for now let me explain the rationale behind the 1000x1000 -&gt; 768x384 reshaping .</p>\n<h2>Why Split the Image?</h2>\n<pre><code>h, w, _ = img.shape  # Original: 2000x1000\nmid = w // 2\nleft = img[:, :mid]   # Left half: 1000x1000\nright = img[:, mid:]  # Right half: 1000x1000\n</code></pre>\n<p><strong>The original images are 2000x1000 (2:1 aspect ratio)</strong> from the paper. Splitting vertically gives you two 1000x1000 square images.</p>\n<h2>Why split?</h2>\n<ul>\n<li><strong>More spatial diversity</strong> - Each half sees different parts of the quadrat, allowing the model to learn features from different regions</li>\n<li><strong>Memory efficiency</strong> - Processing two smaller images uses less GPU memory than one large image</li>\n<li><strong>Aspect ratio alignment</strong> - 1000x1000 is easier to resize to DINO v3's preferred dimensions</li>\n</ul>\n<h2>Why 768x384?</h2>\n<pre><code>IMG_HEIGHT = 768\nIMG_WIDTH = 384\n</code></pre>\n<p><code>DINO v3 uses a patch size of 16x16 pixels</code>. For the model to process an image without needing padding:</p>\n<pre><code>768 / 16 = 48 patches (vertically)\n384 / 16 = 24 patches (horizontally)\n</code></pre>\n<p><strong>Both dimensions are perfectly divisible by 16</strong>, so no padding or information loss from patch boundaries.</p>\n<p><strong>The 2:1 ratio is maintained:</strong></p>\n<ul>\n<li>Original half: 1000x1000</li>\n<li>Resized to: 768x384 (still 2:1 ratio)</li>\n<li>Full image processing covers: 1536x768 effective area (the two halves combined)</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F21739094%2Fbd1185f5af4d21c929a5435c579bff1d%2Fmermaid-diagram-2026-01-29-150122.png?generation=1769681696172342&amp;alt=media\" alt=\"\"></p>\n<h2>Why Not Other Sizes?</h2>\n<p>If you used 512x512:</p>\n<ul>\n<li>Would need more memory</li>\n<li>Would lose more information</li>\n<li>Still divisible by 16 (512/16 = 32 patches)</li>\n</ul>\n<p><strong>768x384 is a sweet spot:</strong></p>\n<ul>\n<li>Large enough to capture pasture details</li>\n<li>Fits in GPU memory (batch size 4)</li>\n<li>Maintains useful information</li>\n<li>DINO v3 was pre-trained on similar resolutions</li>\n</ul>\n<p><strong>The reshaping isn't really magic - it's practical engineering:</strong></p>\n<ol>\n<li>Split to get more spatial perspectives from the same image.</li>\n<li>Resize to dimensions that work perfectly with DINO v3's patch-based architecture.</li>\n<li>Process each half independently, then combine features.</li>\n</ol>\n<p>This approach gave you more training signal per image without overwhelming GPU memory.</p>",
      "rawMarkdown": "arturkhodakovskiy  Great question! I advise you must read the competition paper too for better understanding but for now let me explain the rationale behind the 1000x1000 -> 768x384 reshaping .\n\n## Why Split the Image?\n\n```python\nh, w, _ = img.shape  # Original: 2000x1000\nmid = w // 2\nleft = img[:, :mid]   # Left half: 1000x1000\nright = img[:, mid:]  # Right half: 1000x1000\n```\n**The original images are 2000x1000 (2:1 aspect ratio)** from the paper. Splitting vertically gives you two 1000x1000 square images.\n\n## Why split?\n\n- **More spatial diversity** - Each half sees different parts of the quadrat, allowing the model to learn features from different regions\n- **Memory efficiency** - Processing two smaller images uses less GPU memory than one large image\n- **Aspect ratio alignment** - 1000x1000 is easier to resize to DINO v3's preferred dimensions\n\n## Why 768x384?\n\n```python\nIMG_HEIGHT = 768\nIMG_WIDTH = 384\n```\n`DINO v3 uses a patch size of 16x16 pixels`. For the model to process an image without needing padding:\n\n```python\n768 / 16 = 48 patches (vertically)\n384 / 16 = 24 patches (horizontally)\n```\n**Both dimensions are perfectly divisible by 16**, so no padding or information loss from patch boundaries.\n\n**The 2:1 ratio is maintained:**\n\n- Original half: 1000x1000\n- Resized to: 768x384 (still 2:1 ratio)\n- Full image processing covers: 1536x768 effective area (the two halves combined)\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F21739094%2Fbd1185f5af4d21c929a5435c579bff1d%2Fmermaid-diagram-2026-01-29-150122.png?generation=1769681696172342&alt=media)\n\n## Why Not Other Sizes?\n\nIf you used 512x512:\n\n- Would need more memory\n- Would lose more information\n- Still divisible by 16 (512/16 = 32 patches)\n\n**768x384 is a sweet spot:**\n\n- Large enough to capture pasture details\n- Fits in GPU memory (batch size 4)\n- Maintains useful information\n- DINO v3 was pre-trained on similar resolutions\n\n**The reshaping isn't really magic - it's practical engineering:**\n\n1. Split to get more spatial perspectives from the same image.\n2. Resize to dimensions that work perfectly with DINO v3's patch-based architecture.\n3. Process each half independently, then combine features.\n\nThis approach gave you more training signal per image without overwhelming GPU memory.",
      "votes": null
    },
    {
      "id": "3398610",
      "postDate": "01/29/2026 11:50:44",
      "content": "<p>Thank you for your answer! </p>\n<p>Now I think it’s mostly a matter of your approach. I did FiLM tiling 4×4 and FiLM 2×2 and concatenated them (also reshaped patches to the closest size divisible by the model patch size, with dynamic_img_size=True). But my main problem was that I relied too much on my CV scores, and I should have spent more time on the CV split strategy, which helped a lot for people with similar model strategies as mine.</p>",
      "rawMarkdown": "Thank you for your answer! \n\nNow I think it’s mostly a matter of your approach. I did FiLM tiling 4×4 and FiLM 2×2 and concatenated them (also reshaped patches to the closest size divisible by the model patch size, with dynamic_img_size=True). But my main problem was that I relied too much on my CV scores, and I should have spent more time on the CV split strategy, which helped a lot for people with similar model strategies as mine.",
      "votes": null
    }
  ],
  "comments": [
    {
      "id": 3398341,
      "author_name": "yigecaiji",
      "author_url": "",
      "post_date": "01/29/2026 02:03:39",
      "content": "<p>Congrats! The dual-pathway design combining DINO v3 and SigLIP-GBDT is excellently executed. Nice job!</p>",
      "votes": null,
      "replies": []
    },
    {
      "id": 3398342,
      "author_name": "arturkhodakovskiy",
      "author_url": "",
      "post_date": "01/29/2026 02:06:40",
      "content": "<p>Thank you for your detailed solution description! </p>\n<p>Could you please give me your understanding to reshape 1000x1000 -&gt; 768x384? My teammate discovered such hard reshaping too, but for me it was some sort of magic trick without natural understanding “How it really works”</p>",
      "votes": null,
      "replies": [
        {
          "id": 3398560,
          "author_name": "",
          "author_url": "",
          "post_date": "01/29/2026 10:19:53",
          "content": "<p><a href=\"https://www.kaggle.com/arturkhodakovskiy\" target=\"_blank\">@arturkhodakovskiy</a>  Great question! I advise you must read the competition paper too for better understanding but for now let me explain the rationale behind the 1000x1000 -&gt; 768x384 reshaping .</p>\n<h2>Why Split the Image?</h2>\n<pre><code>h, w, _ = img.shape  # Original: 2000x1000\nmid = w // 2\nleft = img[:, :mid]   # Left half: 1000x1000\nright = img[:, mid:]  # Right half: 1000x1000\n</code></pre>\n<p><strong>The original images are 2000x1000 (2:1 aspect ratio)</strong> from the paper. Splitting vertically gives you two 1000x1000 square images.</p>\n<h2>Why split?</h2>\n<ul>\n<li><strong>More spatial diversity</strong> - Each half sees different parts of the quadrat, allowing the model to learn features from different regions</li>\n<li><strong>Memory efficiency</strong> - Processing two smaller images uses less GPU memory than one large image</li>\n<li><strong>Aspect ratio alignment</strong> - 1000x1000 is easier to resize to DINO v3's preferred dimensions</li>\n</ul>\n<h2>Why 768x384?</h2>\n<pre><code>IMG_HEIGHT = 768\nIMG_WIDTH = 384\n</code></pre>\n<p><code>DINO v3 uses a patch size of 16x16 pixels</code>. For the model to process an image without needing padding:</p>\n<pre><code>768 / 16 = 48 patches (vertically)\n384 / 16 = 24 patches (horizontally)\n</code></pre>\n<p><strong>Both dimensions are perfectly divisible by 16</strong>, so no padding or information loss from patch boundaries.</p>\n<p><strong>The 2:1 ratio is maintained:</strong></p>\n<ul>\n<li>Original half: 1000x1000</li>\n<li>Resized to: 768x384 (still 2:1 ratio)</li>\n<li>Full image processing covers: 1536x768 effective area (the two halves combined)</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F21739094%2Fbd1185f5af4d21c929a5435c579bff1d%2Fmermaid-diagram-2026-01-29-150122.png?generation=1769681696172342&amp;alt=media\" alt=\"\"></p>\n<h2>Why Not Other Sizes?</h2>\n<p>If you used 512x512:</p>\n<ul>\n<li>Would need more memory</li>\n<li>Would lose more information</li>\n<li>Still divisible by 16 (512/16 = 32 patches)</li>\n</ul>\n<p><strong>768x384 is a sweet spot:</strong></p>\n<ul>\n<li>Large enough to capture pasture details</li>\n<li>Fits in GPU memory (batch size 4)</li>\n<li>Maintains useful information</li>\n<li>DINO v3 was pre-trained on similar resolutions</li>\n</ul>\n<p><strong>The reshaping isn't really magic - it's practical engineering:</strong></p>\n<ol>\n<li>Split to get more spatial perspectives from the same image.</li>\n<li>Resize to dimensions that work perfectly with DINO v3's patch-based architecture.</li>\n<li>Process each half independently, then combine features.</li>\n</ol>\n<p>This approach gave you more training signal per image without overwhelming GPU memory.</p>",
          "votes": null,
          "replies": [
            {
              "id": 3398610,
              "author_name": "arturkhodakovskiy",
              "author_url": "",
              "post_date": "01/29/2026 11:50:44",
              "content": "<p>Thank you for your answer! </p>\n<p>Now I think it’s mostly a matter of your approach. I did FiLM tiling 4×4 and FiLM 2×2 and concatenated them (also reshaped patches to the closest size divisible by the model patch size, with dynamic_img_size=True). But my main problem was that I relied too much on my CV scores, and I should have spent more time on the CV split strategy, which helped a lot for people with similar model strategies as mine.</p>",
              "votes": null,
              "replies": []
            }
          ]
        }
      ]
    }
  ],
  "raw_markdown_by_id": {
    "3398326": "**Author:** Zulqarnain Ali \n\n**Competition:** CSIRO - Image2Biomass Prediction  \n**Final Ranking:** Private LB 26th / Public LB 105th  \n---\n\n## My Approach at a Glance\n\nLike many other i too used DINO .I built a dual-pathway system that combines two very different approaches:\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9ecb63fd1c14cf98aced2be51cb846c2%2Fmermaid-diagram-2026-01-29-061928.png?generation=1769649584396841&alt=media)\n\nThe main thing  was that DINO v3 and SigLIP learn completely different things from images. DINO excels at visual patterns (what green looks like versus what dead grass looks like), while SigLIP understands semantic concepts. Combining them gave me better results than either alone.\n\n\n## Dataset \n\nBased on the competition paper, I learned that this dataset is actually pretty special in the agricultural AI world:\n\n- **1,162 images** that passed strict quality control (from an initial 3,187 collected)\n- **19 sites** across NSW, Victoria, Tasmania, and Western Australia\n- **Multiple seasons** from 2014-2017\n- **Multiple camera types** including consumer devices like iPhone 4/5s, which makes the data more realistic\n\nThe paper mentions that NDVI values were collected using a GreenSeeker handheld sensor held about 1 meter above the quadrat, with 100 readings averaged. They also measured pasture height with a falling plate meter. These weren't available at test time, so I focused purely on visual features.\n\nFor the targets, I paid special attention to the relationships:\n- **GDM_g = Green + Clover** (Green Dry Matter - the digestible stuff)\n- **Total = GDM + Dead** (everything available to eat)\n\nThis meant my predictions needed to maintain these relationships, or the post-processing would fix them.\n\n\n## Model Architecture\n\n### The DINO v3 Backbone\n\nI used DINO v3 huge (`vit_huge_plus_patch16_dinov3.lvd1689m`) as my backbone. The image splitting strategy was crucial - I split each 2000x1000 image vertically in half, processed each half separately through the same backbone, then concatenated the features.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fa500c213db078ac0d37ba3fddcf85618%2Fmermaid-diagram-2026-01-29-062822.png?generation=1769650120965240&alt=media)\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F9a25dd4054446dfd3322d25de8506e17%2Fmermaid-diagram-2026-01-29-062911.png?generation=1769650166008030&alt=media)\n\n### The Local Mamba Block\n\nFor feature fusion, I experimented with several options and found that Local Mamba blocks worked well. This is a lightweight attention-like mechanism:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F5c4384db3e57fa5fec12ebe284c2adf3%2Fmermaid-diagram-2026-01-29-060909.png?generation=1769648968305823&alt=media)\n\nThe depthwise convolution captures local patterns efficiently, and the gating mechanism controls information flow while maintaining gradient flow through the residual connection.\n\n### SigLIP Semantic Features\n\nThe second pathway uses SigLIP SO400M for semantic understanding. I extracted embeddings and computed similarity scores against agricultural concepts:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F21855fb7016bbc3db22d42c72728b9c5%2Fmermaid-diagram-2026-01-29-061033.png?generation=1769649044772177&alt=media)\n\nThe concept-based scoring helps the model understand pasture composition in semantic terms, not just raw pixels.\n\n---\n\n## Training Strategy\n\n### Cross-Validation\n\nI used Stratified Group K-Fold with 3 folds, stratifying on `Dry_Total_g` quintiles and grouping by image ID to prevent data leakage:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F1faac336a6241dcd4d475d1298eca6d1%2Fmermaid-diagram-2026-01-29-061154.png?generation=1769649126810187&alt=media)\n\n\n### Training Config\n\nHere's what worked for me:\n\n| Parameter | Value | Why |\n|-----------|-------|-----|\n| Image Size | 768x384 | Balance detail vs memory |\n| Batch Size | 4 | GPU memory limit |\n| Epochs | 180 | Full training run |\n| LR Backbone | 1e-5 | Conservative for pretrained |\n| LR Head | 5e-4 | Higher for new heads |\n| Dropout | 0.2 | Prevent overfitting |\n| Early Stop | 30 epochs | Regularization |\n\n### Data Augmentation\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2Fcd34a9cd81a53ba7462a7f18a5384ac3%2Fmermaid-diagram-2026-01-29-061300.png?generation=1769649193655864&alt=media)\n\n### Loss Function\n\nI designed a custom loss that matches the competition metric:\n\n```python\nclass WeightedBiomassLoss(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.weights = torch.tensor([0.1, 0.1, 0.1, 0.2, 0.5])\n        self.huber = nn.SmoothL1Loss(reduction='none', beta=5.0)\n\n    def forward(self, preds, labels):\n        preds_log = torch.log1p(preds)\n        labels_log = torch.log1p(labels)\n        loss = self.huber(preds_log, labels_log)\n        weighted_loss = (loss * self.weights.to(loss.device)).mean()\n        return weighted_loss\n```\n\nThe log transform stabilizes the wide range of biomass values, and Huber loss handles outliers gracefully.\n\n---\n\n## Inference Pipeline\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F21739094%2F2d57241ac810573ab579e2b39c706c73%2Fmermaid-diagram-2026-01-29-061411.png?generation=1769649272533056&alt=media)\n\nPost-processing included:\n- Clipping all predictions to non-negative values\n- Recalculating GDM_g and Total_g from components\n- Applying small adjustments to Dead based on its predicted range\n\n---\n\n## Results\n\n### Cross-Validation Scores\n\n| Fold | CV R² |\n|------|-------|\n| 0 | 0.838 |\n| 1 | 0.871 |\n| 2 | 0.822 |\n| **Mean** | **0.844 ± 0.021** |\n\n\n---\n\n## What I Learned\n\n1. **Image splitting helps** - Processing left and right halves separately gave the model more spatial flexibility than trying to learn from the whole image at once.\n\n2. **DINO + SigLIP is a powerful combo** - DINO's visual patterns + SigLIP's semantic understanding captured different aspects of pasture composition.\n\n3. **Respect the target relationships** - GDM and Total being derived meant I had to ensure my predictions maintained these relationships or fix them in post-processing.\n\n4. **Regularization was essential** - With ~1000 training images, dropout, weight decay, and early stopping prevented overfitting.\n\n5. **The paper matters** - Understanding the data collection process helped me design better preprocessing and augmentation strategies.\n\n## The complete code can be found here : https://github.com/zulqarnainalipk/CSIRO-Biomass-Prediction",
    "3398341": "Congrats! The dual-pathway design combining DINO v3 and SigLIP-GBDT is excellently executed. Nice job!",
    "3398342": "Thank you for your detailed solution description! \n\nCould you please give me your understanding to reshape 1000x1000 -> 768x384? My teammate discovered such hard reshaping too, but for me it was some sort of magic trick without natural understanding “How it really works”",
    "3398560": "arturkhodakovskiy  Great question! I advise you must read the competition paper too for better understanding but for now let me explain the rationale behind the 1000x1000 -> 768x384 reshaping .\n\n## Why Split the Image?\n\n```python\nh, w, _ = img.shape  # Original: 2000x1000\nmid = w // 2\nleft = img[:, :mid]   # Left half: 1000x1000\nright = img[:, mid:]  # Right half: 1000x1000\n```\n**The original images are 2000x1000 (2:1 aspect ratio)** from the paper. Splitting vertically gives you two 1000x1000 square images.\n\n## Why split?\n\n- **More spatial diversity** - Each half sees different parts of the quadrat, allowing the model to learn features from different regions\n- **Memory efficiency** - Processing two smaller images uses less GPU memory than one large image\n- **Aspect ratio alignment** - 1000x1000 is easier to resize to DINO v3's preferred dimensions\n\n## Why 768x384?\n\n```python\nIMG_HEIGHT = 768\nIMG_WIDTH = 384\n```\n`DINO v3 uses a patch size of 16x16 pixels`. For the model to process an image without needing padding:\n\n```python\n768 / 16 = 48 patches (vertically)\n384 / 16 = 24 patches (horizontally)\n```\n**Both dimensions are perfectly divisible by 16**, so no padding or information loss from patch boundaries.\n\n**The 2:1 ratio is maintained:**\n\n- Original half: 1000x1000\n- Resized to: 768x384 (still 2:1 ratio)\n- Full image processing covers: 1536x768 effective area (the two halves combined)\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-forum-message-attachments/o/inbox%2F21739094%2Fbd1185f5af4d21c929a5435c579bff1d%2Fmermaid-diagram-2026-01-29-150122.png?generation=1769681696172342&alt=media)\n\n## Why Not Other Sizes?\n\nIf you used 512x512:\n\n- Would need more memory\n- Would lose more information\n- Still divisible by 16 (512/16 = 32 patches)\n\n**768x384 is a sweet spot:**\n\n- Large enough to capture pasture details\n- Fits in GPU memory (batch size 4)\n- Maintains useful information\n- DINO v3 was pre-trained on similar resolutions\n\n**The reshaping isn't really magic - it's practical engineering:**\n\n1. Split to get more spatial perspectives from the same image.\n2. Resize to dimensions that work perfectly with DINO v3's patch-based architecture.\n3. Process each half independently, then combine features.\n\nThis approach gave you more training signal per image without overwhelming GPU memory.",
    "3398610": "Thank you for your answer! \n\nNow I think it’s mostly a matter of your approach. I did FiLM tiling 4×4 and FiLM 2×2 and concatenated them (also reshaped patches to the closest size divisible by the model patch size, with dynamic_img_size=True). But my main problem was that I relied too much on my CV scores, and I should have spent more time on the CV split strategy, which helped a lot for people with similar model strategies as mine."
  },
  "source": "meta"
}