老实说,我仍然无法相信这一切。我的第一枚银牌。在 3000 多支队伍中排名第 33。我不得不刷新了三次排行榜以确保这是真的 :)
首先,感谢 CSIRO 以及所有参与组织这次竞赛的人员。从图像中预测牧场生物量是一个非常现实世界的问题,我在工作中学到了很多。
快速致谢那些帮助我一路走来的公开 Notebook 和数据集 —— SigLIP 模型、数据划分 Notebook 以及许多我们赖以构建的基线方法。Kaggle 社区真的很棒。
我的历程
我大约在一个月前参加了这次竞赛,但老实说,我大部分 serius 的工作都是在最后 15 天完成的。在前几周,我只是阅读讨论区,运行公开 Notebook,试图理解什么有效,什么无效。
包括我在内的许多人的公共排行榜得分都卡在 0.72-0.73 左右。我不断尝试各种小改动,但没有什么真正能推动进展。然后我开始换个角度思考。
改变一切的主要想法是:与其像其他人一样使用冻结的 DINO 嵌入,如果我实际上在这个特定数据集上从头重新训练 DINO 模型会怎样?而且不仅仅是任何 DINO,而是拥有 11 亿参数的 Huge 版本。
这很有风险,因为在只有 357 张图像上训练如此庞大的模型很容易过拟合。但通过适当的正则化和早停,它奏效了。我从 0.73 提升到了 0.74,并从公共排行榜的第 106 名跃升至私有排行榜的第 33 名。
我具体做了什么
模型
我使用 vit_huge_plus_patch16_dinov3 作为我的骨干网络。这是一个拥有约 11 亿参数的视觉 Transformer (ViT),使用 DINOv3 自监督学习进行预训练。
每个牧场图像为 2000x1000 像素。我将其垂直分为左半部分和右半部分,每部分 1000x1000。两个部分都通过相同的骨干网络(共享权重),然后将特征拼接在一起。
为了融合这些特征,我使用了称为局部 Mamba 块 (Local Mamba Blocks) 的东西。这基本上是一种带有深度卷积和 gating 机制的轻量级注意力机制:
class LocalMambaBlock(nn.Module):
def __init__(self, dim, kernel_size=5, dropout=0.1):
super().__init__()
self.norm = nn.LayerNorm(dim)
self.dwconv = nn.Conv1d(dim, dim, kernel_size,
padding=kernel_size//2, groups=dim)
self.gate = nn.Linear(dim, dim)
self.proj = nn.Linear(dim, dim)
self.drop = nn.Dropout(dropout)
def forward(self, x):
shortcut = x
x = self.norm(x)
g = torch.sigmoid(self.gate(x))
x = x * g
x = self.dwconv(x.transpose(1, 2)).transpose(1, 2)
x = self.proj(x)
return shortcut + self.drop(x)
想法很简单:归一化,控制信息流,应用局部卷积,投影回去,并添加残差连接。堆叠两个这样的块给了我很好的特征融合效果。
融合之后,我有三个独立的预测头分别用于 Green (绿草)、Dead (枯草) 和 Clover (三叶草)。然后从中计算 GDM 和 Total (GDM = Green + Clover, Total = GDM + Dead)。这尊重了目标之间的实际物理关系。
训练细节
以下是对我有效的配置:
- 图像大小:512x512
- Batch size: 6
- 4 折交叉验证,使用 StratifiedGroupKFold
- 学习率:骨干网络 1e-5,头部 5e-4
- 权重衰减:1e-2
- Dropout: 0.2
- 早停耐心值:15 个 epoch
- 最大 epoch: 210 (但早停很早就触发了)
- 带有 2 个 epoch 预热 (warmup) 的余弦退火调度器
- 混合精度训练 (fp16)
对于增强,我保持简单:
- 水平翻转
- 垂直翻转
- 随机旋转 90 度
- 平移缩放旋转
- 颜色抖动 (轻微)
损失函数经过加权以匹配竞赛指标。我使用 log 空间中的 Huber 损失来处理广泛的生物量值和异常值:
weights = [0.1, 0.1, 0.1, 0.2, 0.5] # Green, Dead, Clover, GDM, Total
loss = weighted_mean(HuberLoss(log1p(pred), log1p(target)))
集成模型
单独的 DINO 让我达到了 0.72-0.73 左右。为了进一步推进,我将其与 SigLIP 结合。
SigLIP 是一个视觉 - 语言模型。我从图像中提取嵌入,并计算与农业文本概念(如"bare soil"裸露土壤、"dense pasture"茂密牧场、"dead grass"枯草、"white clover"白三叶草等)的相似性得分。然后将这些语义特征输入到 GBDT 集成中(HistGradientBoosting, GradientBoosting, CatBoost, LightGBM 平均在一起)。
最终预测对于大多数目标是 75% DINO 和 25% SigLIP。对于 Clover specifically,我使用 100% DINO,因为 SigLIP 在预测它方面表现不佳。
后处理
一些有帮助的小校准:
# Clover 倾向于预测过高
Dry_Clover_g = Dry_Clover_g * 0.8
# 根据预测范围调整枯草材料
if Dry_Dead_g > 20:
Dry_Dead_g = Dry_Dead_g * 1.1
elif Dry_Dead_g < 10:
Dry_Dead_g = Dry_Dead_g * 0.9
# 强制质量平衡
GDM_g = Dry_Green_g + Dry_Clover_g
Dry_Total_g = GDM_g + Dry_Dead_g
结果
交叉验证得分:
| 折数 (Fold) | R2 得分 |
|---|---|
| 0 | 0.82 |
| 1 | 0.85 |
| 2 | 0.81 |
| 3 | 0.84 |
| 平均 | 0.83 |
排行榜:
| 阶段 | 得分 | 排名 |
|---|---|---|
| 公共排行榜 (Public LB) | 0.73 | 第 105 名 |
| 私有排行榜 (Private LB) | 0.74 | 第 33 名 |
名次震荡是真实的。相信交叉验证胜过公共排行榜得到了回报。
我的收获
- 重新训练优于冻结特征 - 在这个特定领域微调 DINO 是最大的增益
- 尊重目标之间的关系 - GDM 和 Total 是衍生量,预测应保持这种关系
- 简单的增强即可生效 - 在这个小数据集上,重度增强弊大于利
- 正则化至关重要 - Dropout、权重衰减和早停防止了在 357 张图像上的过拟合
- 相信你的交叉验证 - 公共排行榜具有误导性,私有排行榜与我的交叉验证更匹配
代码与资源
我正在分享我的完整解决方案:
训练代码:一个干净的单细胞 Notebook,包含完整的 DINO 训练管道。我私下运行了这个,所以 Notebook 没有显示我实际的训练输出,但代码正是产生我 0.74 提交的内容。
推理代码:结合 DINO 和 SigLIP 的集成推理。这是生成最终 submission.csv 文件的内容。
模型检查点:训练好的模型权重(4 折),您可以直接用于推理。
GitHub 仓库:包含训练和推理脚本的完整代码仓库。
感谢阅读。这是我的第一枚银牌,对我来说意义重大。如果您对解决方案有任何问题,欢迎在评论中提问。
PS: 我现在是 Kaggle 竞赛专家 (Competitions Expert) :)
Muhammad Ibrahim Qasmi