返回列表

(2nd) Weakly supervised semantic segmentation + Synthetic data

687. CSIRO - Image2Biomass Prediction | csiro-biomass

开始: 2025-10-28 结束: 2026-01-28 作物智能识别 数据算法赛
(第 2 名) 弱监督语义分割 + 合成数据
作者: Quan Vu
竞赛排名: 第 2 名
标题: (第 2 名) 弱监督语义分割 + 合成数据
副标题: 除了已知的传统技术外,我们如何解决图像回归问题?

感谢组织者举办这次比赛。这是我很久以来第一次看到传统的计算机视觉比赛。当这次比赛刚开始时,我在想除了以前用过的传统技术外,该如何解决这个图像回归问题。

1. 交叉验证策略

在这次比赛中,我使用了一个非常简单的验证策略:按州(State)分层,并选择一个种子,使得所有 5 折的标签均值和标准差相似。主要有几个原因:首先,数据集太小,如果我们以极端方式拆分,例如按日期或按州分组,标签分布会变得严重失真。其次,主办方已确认公共和私有测试集中没有新物种。有些州有某些物种,而其他州没有。按日期分组也有风险,可能会遗漏某些物种或训练集中没有代表所有四个季节。据我了解,每个日期组对应于去特定农场收集样本的一次行程,有可能某些三叶草物种只存在于某些农场。

基本上,我的拆分几乎肯定存在数据泄露,我接受这一点。为了减轻这种情况,我使用 CV-LB 相关性和 LB 分数来评估模型。

2. 合成数据集

从比赛一开始,我就专注于生成额外训练数据的方法。然而,我最初的几乎所有实验都失败了:使用合成数据训练模型时,公共排行榜分数 consistently 下降。

使用合成数据集进行图像回归的主要挑战之一是真值(ground truth)问题;当图像被修改时,每个目标的生物量值也会相应变化。为了解决这个问题,我采用了一种伪标签策略:我使用在原始训练集上训练的模型为合成数据集生成标签,随后在组合数据集(原始训练集 + 合成数据集)上训练最终模型。

我使用 Qwen Image Edit 创建了两类合成数据集:

  1. 通过模拟季节性天气变化并添加砾石和岩石等伪影来生成合成数据集。
  2. 我从图像中移除了所有草植被,仅保留裸露的土壤,并将三叶草、枯死和绿色的目标值设置为 0。然后将这些图像与训练数据集结合,以帮助模型区分含生物量区域和非生物量区域。
合成数据示例

3. 数据增强

版本 1

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),
    ])

版本 2

首先,我使用了这些增强:

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 和公共排行榜分数。

最令人惊讶的是,RandomResizedCropRandomErasing 在提高 CV 和公共排行榜分数方面发挥了至关重要的作用。起初,我以为这些增强会改变图像标签从而降低分数。但事实并非如此。也许小的随机裁剪范围反映了这样一个事实,即在现实中,裁剪的草地区域并不总是恰好 70×30,而是在该尺寸附近略有波动。

图像尺寸

我使用了两种类型的图像尺寸。

类型 宽 x 高
1 2048 x 1024
2 1024 x 1024

4. 优化目标函数

损失函数

我使用了 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

5. 模型

骨干网络

DINOv3 的高分辨率密集特征确实非常出色。在密度估计任务中,具有高判别力的详细特征至关重要。我尝试了各种骨干网络,包括现代的网络如 dinov2, XCiT, SigLIP 和 PE spatial (Perception Encoder),但与 DINOv3 相比,CV 和 LB 分数都显著较低。

虽然我想确保多样性,但我只能从 DINOv3 家族中选择骨干网络,其中 ConvNeXt-Large 的表现明显不如 ViT-Large。

弱监督语义分割

我的想法是,不要仅仅将其视为标准的图像回归问题,而是想将其作为密度估计问题来处理。因此,我使用 DINOv3 提取大小为 (N, C, H, W) 的特征图。然后,我尝试附加不同的头来估计密度图。最终结果将是三叶草、枯死和绿色的目标值。

第一次尝试:简单的 Conv + ReLU 头

        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)

第二次尝试:DPT 头

我想将模型设计为语义分割模型,并找到了 SegDino。通过利用多个特征图并采用 SegDino 的 DPT 头,我设计了一个模型,输出一个热力图,其中每个像素对应图像中 16x16 patch 的目标值。

骨干网络

  • 提取索引为 [4, 11, 17, 23] 的中间特征。

DPT 头

  • 将 4 个输入特征图投影到 [96, 192, 384, 768] 通道。
  • 使用 3x3 卷积("Scratch"层)进行细化。
  • 上采样低分辨率特征并将它们连接起来。
  • 最终的 1x1 卷积将通道减少到 3(对应 3 个类别)。

双路径策略:

  • 输入图像被分为左半部分和右半部分。
  • 每一半都独立通过骨干网络和头进行处理。
  • 结果进行全局池化然后相加。
模型结构图
    ...
    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,而较深的颜色表示值更接近最大值。结果看起来很有希望。

三叶草目标的热力图

三叶草热力图

枯死目标的热力图

枯死热力图

绿色目标的热力图

绿色热力图

6. 后处理

基于州的比例缩放后处理

我尝试按州对图像进行分类,令人惊讶的是准确率超过了 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)

7. 推理

分离的目标模型

在训练过程中,我注意到 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 分钟

8. 尝试寻找不同的方法但未成功

  1. 基于解码的回归:链接
  2. 我尝试使用 SAM 2 进行分割。然而,由于三叶草和草非常小且紧密交织,使用 SAM 2 进行有效分割几乎是不可能的。
  3. 我尝试使用 DiffuseMix,但如前所述,合成数据的问题在于当图像改变时,标签也会随之改变。
  4. 使用 Excess Green 指数进行分割,然后将掩码作为第 4 个通道。公共分数没有提高。
  5. 由于生物量估计问题依赖于植物密度和高度,Height_Ave_cm 被用作辅助损失。我还尝试在双流模型中使用 Depth Anything v3 + DINOv3 ViT-Large。所有方法都降低了公共分数。
  6. 使用 Pre_GSHH_NDVI, State, Species 和 Height_Ave_cm 作为辅助损失。所有结果都没有变化或导致公共分数下降。
  7. 在外部数据集上预训练,然后在训练数据集上微调;公共分数相对显著下降。
  8. 测试时训练适应:放弃了,因为推理期间太耗时。
  9. Hflip TTA 没有效果或使用 6-4 权重时影响可忽略不计。
同比赛其他方案