687. CSIRO - Image2Biomass Prediction | csiro-biomass
感谢组织者举办这次比赛。这是我很久以来第一次看到传统的计算机视觉比赛。当这次比赛刚开始时,我在想除了以前用过的传统技术外,该如何解决这个图像回归问题。
在这次比赛中,我使用了一个非常简单的验证策略:按州(State)分层,并选择一个种子,使得所有 5 折的标签均值和标准差相似。主要有几个原因:首先,数据集太小,如果我们以极端方式拆分,例如按日期或按州分组,标签分布会变得严重失真。其次,主办方已确认公共和私有测试集中没有新物种。有些州有某些物种,而其他州没有。按日期分组也有风险,可能会遗漏某些物种或训练集中没有代表所有四个季节。据我了解,每个日期组对应于去特定农场收集样本的一次行程,有可能某些三叶草物种只存在于某些农场。
基本上,我的拆分几乎肯定存在数据泄露,我接受这一点。为了减轻这种情况,我使用 CV-LB 相关性和 LB 分数来评估模型。
从比赛一开始,我就专注于生成额外训练数据的方法。然而,我最初的几乎所有实验都失败了:使用合成数据训练模型时,公共排行榜分数 consistently 下降。
使用合成数据集进行图像回归的主要挑战之一是真值(ground truth)问题;当图像被修改时,每个目标的生物量值也会相应变化。为了解决这个问题,我采用了一种伪标签策略:我使用在原始训练集上训练的模型为合成数据集生成标签,随后在组合数据集(原始训练集 + 合成数据集)上训练最终模型。
我使用 Qwen Image Edit 创建了两类合成数据集:
train_transform = A.ReplayCompose([
A.RandomResizedCrop(size=(1024, 1024), scale=(0.8, 1.0), interpolation=cv2.INTER_CUBIC, p=1),
A.HorizontalFlip(p=0.5),
A.VerticalFlip(p=0.5),
A.RandomRotate90(p=0.5),
A.Rotate(limit=(-5, 5), p=0.7),
A.RandomGamma(gamma_limit=(80, 120), p=0.5),
A.RandomBrightnessContrast(p=0.75),
A.HueSaturationValue(hue_shift_limit=10,
sat_shift_limit=30,
val_shift_limit=20, p=0.5),
A.OneOf([
A.CLAHE(clip_limit=(1, 4), tile_grid_size=(8, 8), p=0.5),
A.Equalize(p=0.5),
], p=0.25),
A.OneOf([
A.Sharpen(p=0.5),
A.Emboss(p=0.5),
], p=0.25),
])
首先,我使用了这些增强:
train_transform = A.Compose([
A.RandomResizedCrop(size=(self.train_imgsize, self.train_imgsize), scale=(0.8, 1.0), interpolation=cv2.INTER_CUBIC, p=1),
A.HorizontalFlip(p=0.5),
A.RandomRotate90(p=0.5),
A.VerticalFlip(p=0.5),
])
之后,增强后的图像会进一步使用 RandAugment 处理。
random_erase = timm.data.random_erasing.RandomErasing(probability=0.5)
我还使用了 Mixup 和 CutMix,α=0.4;这两种增强都有助于提高 CV 和公共排行榜分数。
最令人惊讶的是,RandomResizedCrop 和 RandomErasing 在提高 CV 和公共排行榜分数方面发挥了至关重要的作用。起初,我以为这些增强会改变图像标签从而降低分数。但事实并非如此。也许小的随机裁剪范围反映了这样一个事实,即在现实中,裁剪的草地区域并不总是恰好 70×30,而是在该尺寸附近略有波动。
我使用了两种类型的图像尺寸。
| 类型 | 宽 x 高 |
|---|---|
| 1 | 2048 x 1024 |
| 2 | 1024 x 1024 |
我使用了 Smooth L1 损失。尝试使用 MSE、二元交叉熵损失或对目标值应用 log 变换均未成功。
criterion_green = torch.nn.SmoothL1Loss()
criterion_clover = torch.nn.SmoothL1Loss()
criterion_dead = torch.nn.SmoothL1Loss()
我使用了带有分层学习率的 Schedule-Free Optimizers
| 超参数 | 值 |
|---|---|
| lr | 0.000017 |
| batch size | 2 |
| weight decay | 0.02 |
| number of epochs | 35 |
backbone_params = [p for p in model.model.parameters() if p.requires_grad]
backbone_ids = set(map(id, backbone_params))
# Identify other parameters
other_params = [p for p in model.parameters() if p.requires_grad and id(p) not in backbone_ids]
param_groups = []
if backbone_params:
param_groups.append({
'params': backbone_params,
'lr': 1e-5,
'weight_decay': configs['weight_decay'],
})
if other_params:
param_groups.append({
'params': other_params,
'lr': configs['lr'],
'weight_decay': configs['weight_decay'],
})
optimizer = schedulefree.AdamWScheduleFree(param_groups, warmup_steps=0)
我的模型预测三个目标:clover(三叶草)、dead(枯死) 和 green(绿色)。尝试使用 5 个目标 clover, dead, green, total, gdm 以及使用 3 个目标 clover, total, gdm 都导致公共分数降低。
| 骨干网络 | 选择的轮次 (epoch) |
|---|---|
| dinov3 vitlarge | 24-26 |
| dinov3 vithuge | 33-34 |
DINOv3 的高分辨率密集特征确实非常出色。在密度估计任务中,具有高判别力的详细特征至关重要。我尝试了各种骨干网络,包括现代的网络如 dinov2, XCiT, SigLIP 和 PE spatial (Perception Encoder),但与 DINOv3 相比,CV 和 LB 分数都显著较低。
虽然我想确保多样性,但我只能从 DINOv3 家族中选择骨干网络,其中 ConvNeXt-Large 的表现明显不如 ViT-Large。
我的想法是,不要仅仅将其视为标准的图像回归问题,而是想将其作为密度估计问题来处理。因此,我使用 DINOv3 提取大小为 (N, C, H, W) 的特征图。然后,我尝试附加不同的头来估计密度图。最终结果将是三叶草、枯死和绿色的目标值。
self.model = timm.create_model('vit_large_patch16_dinov3_qkvb.lvd1689m', pretrained=True, features_only=True)
self.pre_pool_conv = nn.Conv2d(feature_channels, feature_channels, kernel_size=3, padding=1, bias=False)
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
self.reg_head_main = nn.Linear(feature_channels, 3) # clover, dead, green
def forward(self, x):
...
features = self.model(x)
last_feature = features[-1]
last_feature = self.pre_pool_conv(last_feature)
last_feature = F.relu(last_feature)
pooled = self.global_pool(last_feature).flatten(1)
target_values = self.reg_head_main(pooled)
我想将模型设计为语义分割模型,并找到了 SegDino。通过利用多个特征图并采用 SegDino 的 DPT 头,我设计了一个模型,输出一个热力图,其中每个像素对应图像中 16x16 patch 的目标值。
...
self.model = timm.create_model('vit_large_patch16_dinov3_qkvb.lvd1689m', pretrained=True)
self.feature_indices = [4, 11, 17, 23]
self.head = DPTHead(3, self.model.embed_dim, 128, False, out_channels=[96, 192, 384, 768])
self.IMAGENET_DEFAULT_MEAN = torch.tensor([0.485, 0.456, 0.406]).to(device_id)
self.IMAGENET_DEFAULT_STD = torch.tensor([0.229, 0.224, 0.225]).to(device_id)
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
def forward(self, x, return_features=False):
...
w = x.shape[3]
x_l = x[:,:,:,:w//2]
x_r = x[:,:,:,w//2:]
outputs_l, features_l = self.model.forward_intermediates(x_l, indices=self.feature_indices)
outputs_r, features_r = self.model.forward_intermediates(x_r, indices=self.feature_indices)
features_l = self.head(features_l)
features_r = self.head(features_r)
main_logits_l = self.global_pool(features_l).flatten(1)
main_logits_r = self.global_pool(features_r).flatten(1)
main_logits = main_logits_l + main_logits_r
我可视化了每个目标的热力图。白色对应目标值 0,而较深的颜色表示值更接近最大值。结果看起来很有希望。
三叶草目标的热力图
![]()
枯死目标的热力图
![]()
绿色目标的热力图
![]()
我尝试按州对图像进行分类,令人惊讶的是准确率超过了 98%。
同时,我还绘制了按州划分的目标分布。
Label Distribution per Target per State
============================================================
Target: Dry_Clover_g
----------------------------------------
count mean std min 25% 50% 75% max
State
NSW 75.0 0.134641 1.166028 0.0 0.000000 0.00000 0.000000 10.0981
Tas 138.0 6.239966 11.614679 0.0 0.000000 1.08390 6.628400 71.7865
Vic 112.0 7.106464 9.571358 0.0 1.406525 4.05135 8.274350 67.8977
WA 32.0 22.087584 20.214792 0.0 4.302500 15.60500 38.200825 58.8800
Target: Dry_Dead_g
----------------------------------------
count mean std min 25% 50% 75% max
State
NSW 75.0 14.203379 15.625313 0.0 2.600000 8.36140 20.898100 83.8407
Tas 138.0 15.231128 12.431224 0.4 6.047525 11.07405 21.062300 58.7479
Vic 112.0 10.113879 8.856831 0.0 3.393400 7.22240 13.375525 38.8581
WA 32.0 0.000000 0.000000 0.0 0.000000 0.00000 0.000000 0.0000
Target: Dry_Green_g
----------------------------------------
count mean std min 25% 50% 75% max
State
NSW 75.0 56.559313 31.228115 7.6 30.224150 53.50000 72.829800 157.9836
Tas 138.0 15.327257 13.107406 0.0 3.953725 12.97535 22.857150 69.6107
Vic 112.0 25.449273 16.016427 3.0 13.882050 21.58675 33.037750 89.7395
WA 32.0 9.299916 18.789542 0.0 0.000000 0.35000 5.413475 72.1000
很容易看出,来自 WA 州的所有样本的 dead 值都为 0。因此,我应用了一个后处理步骤,将所有 wa_prob > 0.75 的样本的 dead 值设置为 0。
wa_prob = state_probs[:, 2]
dead[wa_prob > 0.75] = 0.0
在比赛的最后几天,我的分数卡在 0.76x 的低段。在阅读了 @kevin1742064161 的 评论 后,我开始怀疑公共数据集可能存在偏差,所以我尝试为每个目标和每个州优化一个比例因子。请注意,我 最初的尝试 缩放预测输出失败了。令人惊讶的是,PB 分数直接从 0.76822 跳到了 0.77937。更令人惊讶的是,这在私有排行榜上也有效,集成模型的私有分数从 0.66153 提高到了 0.67558。
# Public LB optimization: "WA": {"clover": 1.03->1.0, "clover": 1.0->0.97, "clover": 0.97->0.90, "clover": 0.97->0.95,"green": 1.0->0.97, "dead": 1.0->0.97->0.90->0.80}
# Public LB optimization: "VIC": {"clover": 1.0->0.97->0.90, "clover": 0.97->0.90}
# Public LB optimization: "TAS": {"clover": 0.97->1.0, "green": 0.97->1.0}
# Public LB optimization: "NSW": {"clover": 0.97->1.0, "green": 1.0->1.03}
STATE_TARGET_SCALE = {
"NSW": {"clover": 1.0, "dead": 1.0, "green": 1.03},
"Tas": {"clover": 1.0, "dead": 1.0, "green": 1},
"Vic": {"clover": 0.85, "dead": 1, "green": 1},
"WA": {"clover": 0.80, "dead": 0.80, "green": 0.97},
}
使用 np.clip 将 clover, dead 和 green 变量的值限制在特定范围内。最大值是根据训练集计算得出的。
clover = np.clip(clover, 0, 71.7865)
dead = np.clip(dead, 0, 83.8407)
green = np.clip(green, 0, 157.9836)
在训练过程中,我注意到 green 目标通常很早就收敛,然后开始过拟合。green 目标的 RMSE 通常在第 11-15 轮左右下降,然后持续增加。其余的目标,clover 和 dead,通常在第 24-30 轮左右收敛。
因此,我使用第 15 轮的模型来预测 green 目标,使用第 26 轮的模型来预测其他两个目标,clover 和 dead。
| 版本 | 公共排行榜 | 私有排行榜 |
|---|---|---|
| 使用分离的目标模型 | 0.76731 | 0.65615 |
| 不使用分离的目标模型 | 0.76128 | 0.64744 |
| 模型 | 图像尺寸 | 骨干网络 | 公共排行榜 | 私有排行榜 |
|---|---|---|---|---|
| conv_relu + 1 tiles | 2048x1024 | dinov3 vit large | 0.75401 | 0.63349 |
| dpt_head + 1 tiles | 2048x1024 | dinov3 vit large | 0.75844 | 0.62890 |
| conv_relu + 1 tiles | 1280x1280 | dinov3 vit huge | 0.75390 | 0.65361 |
| dpt_head + 2 tiles | 2048x1024 | dinov3 vit large | 0.76379 | 0.64781 |
| dpt_head + 8 tiles | 2048x1024 | dinov3 vit large | 0.75036 | 0.62650 |
| dpt_head + 2 tiles | 2048x1024 | dinov3 vit huge | 0.75994 | 0.66569 |
| conv_relu + 2 tiles + separate target models | 2048x1024 | dinov3 vit large | 0.76731 | 0.65615 |
| conv_relu + 2 tiles | 2048x1024 | dinov3 vit huge | 0.75718 | 0.66731 |
查看上表,我们可以看到具有 ViT-Large 骨干网络的模型通常具有更高的公共分数,但具有 ViT-Huge 骨干网络的模型实现了更高的私有分数。
由于我不确定后处理技术是否也适用于私有数据集,我提交了两个版本:一个带有后处理技术的集成模型,另一个没有后处理技术的集成模型。
| 版本 | 公共排行榜 | 私有排行榜 |
|---|---|---|
| 带有后处理技术 | 0.77839 | 0.67558 |
| 不带后处理技术 | 0.76822 | 0.66153 |
有趣的是,集成模型的私有分数实际上低于单个模型:0.66153 < 0.67156(不带后处理技术)。我已附上了私有分数为 0.67156 的单个模型的 notebook 链接。
我真的无法确切知道哪个单个模型在私有数据集上表现最好,所以即使集成的私有分数不完美,它帮助我避免了分数大幅波动。如果我确切知道哪个单个模型有 0.67156 的私有分数,那么通过集成方法 + 分离目标模型和后处理技术,我可能可以将私有分数提高到 0.69 😅
我使用了 FP16 量化并在两个 T4 GPU 上运行了两个进程。结果,我能够将推理时间从大约 2 小时 减少到 27 分钟。