687. CSIRO - Image2Biomass Prediction | csiro-biomass
副标题:双流类 DINO 模型 + 区间分类 + 测试时在线训练
感谢组织者举办如此激烈的比赛。我们很幸运能获胜,我要感谢我的队友们。这是我第一次完整参加 Kaggle 竞赛,这段经历对我来说非常宝贵 💪。我们的解决方案如下。
分为三个折 (folds),由州和采样日期共同决定,然后训练单折模型。
与大多数优秀的开源 Notebook 类似,我们的模型将图像分为左视图和右视图,并将它们通过相同的主干网络以增强一致性。
我们使用了各种常规的数据增强方法,如下所示:
def get_train_transforms(args):
return Compose([
HorizontalFlip(p=0.5),
VerticalFlip(p=0.5),
RandomRotate90(p=0.5),
A.GaussNoise(p=0.3),
A.RandomBrightnessContrast(
brightness_limit=0.2,
contrast_limit=0.2,
p=0.75
),
A.HueSaturationValue(
hue_shift_limit=10,
sat_shift_limit=20,
val_shift_limit=20,
p=0.5
),
A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=0.3),
A.ColorJitter(
brightness=0.2,
contrast=0.2,
saturation=0.2,
hue=0.1,
p=0.75
),
Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]
),
Resize(args.img_size, args.img_size),
ToTensorV2()
])
在分割左右子图像后,我们独立地对子图像应用数据增强。此外,我们随机对图像应用小尺寸缩放,然后用黑色像素填充,以模拟不同相机的焦距和视角差异。
if random.random() < 0.2:
background_image = np.full_like(image, 0)
resize_retio = random.uniform(0.85, 1.0)
image = cv2.resize(image, None, fx=resize_retio, fy=resize_retio, interpolation=cv2.INTER_CUBIC)
h, w, _ = image.shape
bg_h, bg_w, _ = background_image.shape
top = random.randint(0, bg_h - h)
left = random.randint(0, bg_w - w)
background_image[top:top + h, left:left + w] = image
image = background_image
我们使用 DINOv3 作为骨干网络,这大概是本次竞赛大多数获胜团队使用的方法。随后,我们使用单个多头自注意力层与左右子图像的拼接特征进行交互,然后通过 MLP 输出融合特征。
我们使用五个回归头独立预测每个物种的生物量,每个头包含三个线性层。在训练期间不对它们施加物理约束,我们认为这减少了模型对训练集的依赖。
与大多数解决方案不同,我们还额外设计了五个分类头。鉴于此任务类似于没有密度图的 crowd counting (CC) 问题,我们特意阅读了 CC 领域的大量工作。参考 UEPNet[1] 中的区间划分方案,我们使用训练集为每个物种划分了七个区间:
BORDERS_DICT = {
'Dry_Clover_g': [1.6e-05, 3.9, 10.5353, 20.6523, 37.5911, 71.7865],
'Dry_Dead_g': [1.6e-05, 6.1407, 13.1192, 23.277, 38.8581, 83.8407],
'Dry_Green_g': [1.6e-05, 13.4232, 27.0782, 45.5236, 79.834, 157.9836],
'Dry_Total_g': [1.6e-05, 23.4907, 41.1, 61.1, 96.8288, 185.7],
'GDM_g': [1.6e-05, 16.5143, 30.507, 49.5585, 81.0, 157.9836],
}
基于划分的区间,我们为每个物种添加了一个额外的分类头来预测生物量所在的区间,这使得 LB 和 PB 都提高了约 0.03。这也与我们的 CV 高度一致。理想情况下,如果区间划分得足够小,模型可以通过区间推断值准确预测生物量。(显然,对于这次没有密度图/分割图的竞赛来说,这太难了。)
我们最终提交的模型对回归使用常见的 SmoothL1 Loss,对分类使用 Cross-Entropy loss。每个生物量的损失根据竞赛提出的 R^2 计算权重进行缩放。
class WeightedBiomassLoss(nn.Module):
def __init__(self, loss_weights_dict):
super(WeightedBiomassLoss, self).__init__()
self.criterion = nn.SmoothL1Loss()
self.cls_loss = nn.CrossEntropyLoss(reduction='none')
self.cls_weight = 0.3
self.weights = loss_weights_dict
def forward(self, predictions, targets, predictions_cls=None, cls_labels=None):
total_cls_loss = None
pred_green, pred_dead, pred_clover, pred_gdm, pred_total = predictions
if predictions_cls is not None:
assert cls_labels is not None
pred_green_cls, pred_dead_cls, pred_clover_cls, pred_gdm_cls, pred_total_cls = predictions_cls
cls_green_loss = self.cls_loss(pred_green_cls, cls_labels[:, 0])
cls_dead_loss = self.cls_loss(pred_dead_cls, cls_labels[:, 1])
cls_clover_loss = self.cls_loss(pred_clover_cls, cls_labels[:, 2])
cls_gdm_loss = self.cls_loss(pred_gdm_cls, cls_labels[:, 3])
cls_total_loss = self.cls_loss(pred_total_cls, cls_labels[:, 4])
total_cls_loss = (
self.weights['green_loss'] * cls_green_loss.mean() +
self.weights['dead_loss'] * cls_dead_loss.mean() +
self.weights['clover_loss'] * cls_clover_loss.mean() +
self.weights['gdm_loss'] * cls_gdm_loss.mean() +
self.weights['total_loss'] * cls_total_loss.mean()
)
true_green = targets[:, 0].unsqueeze(-1) # Shape [batch, 1]
true_dead = targets[:, 1].unsqueeze(-1) # Shape [batch, 1]
true_clover = targets[:, 2].unsqueeze(-1) # Shape [batch, 1]
true_gdm = targets[:, 3].unsqueeze(-1) # Shape [batch, 1]
true_total = targets[:, 4].unsqueeze(-1) # Shape [batch, 1]
loss_green = self.criterion(pred_green, true_green)
loss_dead = self.criterion(pred_dead, true_dead)
loss_clover = self.criterion(pred_clover, true_clover)
loss_gdm = self.criterion(pred_gdm, true_gdm)
loss_total = self.criterion(pred_total, true_total)
total_loss = (
self.weights['total_loss'] * loss_total +
self.weights['gdm_loss'] * loss_gdm +
self.weights['green_loss'] * loss_green +
self.weights['dead_loss'] * loss_dead +
self.weights['clover_loss'] * loss_clover # +
)
if predictions_cls is not None:
total_loss = total_loss + self.cls_weight * total_cls_loss
return total_loss, total_cls_loss
还有一些额外的实验最终没有包含在提交的模型中,但 PB 证明了我们在此过程中的设计是有意义的。例如,使用 epsilon-insensitive L1 loss 进行回归优化。这一观察基于生物量分布的不均匀性,我们希望“大值有大误差,小值有小误差”。因此,我们设计了以下基于标签的损失,这使我们能够在 PB 上获得最佳单模型结果。
class EpsilonInsensitiveLoss(nn.Module):
def __init__(self, eps_point=20, max_eps=5, reduction='mean'):
super().__init__()
self.eps_point = eps_point
self.reduction = reduction
self.scale_ratio = 0.1
self.max_eps = max_eps
def make_epsilon(self, target):
epsilon = torch.zeros_like(target)
epsilon = torch.where(
target<=self.eps_point, torch.ones_like(target), epsilon
)
epsilon = torch.where(
target>self.eps_point, target*self.scale_ratio, epsilon
)
epsilon = torch.clamp(epsilon, max=self.max_eps)
return epsilon
def forward(self, pred, target):
epsilon = self.make_epsilon(target.detach())
abs_diff = torch.abs(pred - target)
loss = torch.relu(abs_diff - epsilon)
if self.reduction == 'mean':
return loss.mean()
elif self.reduction == 'sum':
return loss.sum()
return loss
在竞赛早期,我们尝试了各种不同的策略,最终选择了最有效的两阶段训练策略。
此外,在我们的设置下,无论是 LB 还是 CV,我们都发现 DINO-Large 和 DINO-Huge 的性能相似。由于计算资源有限,增加输入尺寸的好处优于增加模型尺寸。因此,在竞赛后期,我们最好的单模型通常使用 1024 像素 + Large 配置进行训练。
| 实验名称 (exp_name) | 输入尺寸 (单视图) | LB | PB |
|---|---|---|---|
| Baseline-large | 512 | 0.70 | 0.60 |
| +cls | 512 | 0.73 | 0.63 |
| +cls | 1024 | 0.74 | 0.64 |
| +aug_scale + cls | 1024 | 0.74 | 0.65 |
| +cls + $\epsilon$-insensitive loss | 512 | 0.71 | 0.66 |
测试期间的在线训练为我们带来了 >0.02 的提升。
我们使用了一些简单的后处理来略微提高 LB 和 PB 的分数,如下所示:
out_green = self.head_green(combined)
out_dead = self.head_dead(combined)
out_clover = self.head_clover(combined)
out_gdm = self.head_gdm(combined)
out_total = self.head_total(combined)
out_clover = out_clover * 0.8
if out_dead > 20:
out_dead *= 1.1
elif out_dead < 10:
out_dead *= 0.9
out_gdm = 0.5*out_gdm+ 0.5*(out_green+out_clover)
pred_total1 = out_green + out_clover + out_dead
pred_total2 = out_gdm + out_dead
out_total = 0.5*out_total + 0.5*pred_total1 + 0.0*pred_total2
| 实验名称 (exp_name) | LB | PB |
|---|---|---|
| PB 最佳 | 0.77 | 0.68 |
| 提交 | 0.77 | 0.67 |
🎉🎉🎉
最后但同样重要的是,祝贺我的队友 @pingfan,Kaggle 社区即将迎来一位新的 GM!!!
🎉🎉🎉
[1] Wang, Changan, Qingyu Song, Boshen Zhang, Yabiao Wang, Ying Tai, Xuyi Hu, Chengjie Wang, Jilin Li, Jiayi Ma, and Yang Wu. "Uniformity in heterogeneity: Diving deep into count interval partition for crowd counting." In Proceedings of the IEEE/CVF international conference on computer vision, pp. 3234-3242. 2021.