返回列表

2nd Place. Journey Down the Rabbit Hole of Pseudo Labels

654. BirdCLEF+ 2025 | birdclef-2025

开始: 2025-03-10 结束: 2025-06-05 环境监测 数据算法赛
2nd Place. Journey Down the Rabbit Hole of Pseudo Labels

第二名:伪标签的兔子洞之旅

作者: Volodymyr (vladimirsydor) & vialactea

竞赛: BirdCLEF 2025

发布日期: 2025-06-08

排名: 第 2 名

我们要感谢乌克兰武装部队、乌克兰国家安全局、乌克兰国防情报局和乌克兰国家紧急服务局,他们提供了安全和保障,使我们能够参与这场伟大的竞赛,完成这项工作,并帮助科学、技术和商业不停滞而是向前发展。

随着竞赛接近尾声,俄罗斯再次对和平的乌克兰城市发动了大规模导弹和无人机袭击。这一恐怖行为针对平民住宅和关键基础设施,造成广泛破坏和人员伤亡。此外,使用“双重打击”战术——两次袭击同一地点以杀害正在援助被困在受损和毁坏建筑物中人员的救援人员——进一步突显了袭击的非人道性。这一行动再次证实了俄罗斯作为一个恐怖主义国家的地位。

开场白

如果我们分解整个 BirdCLEF 竞赛的历程,可以说直到 2023 年(含)都是“额外数据就是一切”的时代,但自 2024 年以来,范式已转变为“伪标签的兔子洞之旅”。所以让我们跳进去,探索半监督学习的仙境。

简短的数据故事

我们下载了额外的 Xeno-Canto 数据,考虑到 2023 年竞赛 中提到的 bug,结果大约有:

来源 样本数量
Xeno Canto 7,376
往届竞赛 90

这些样本不在当年的数据集中,但具有适当的 primary_label 标签。
有趣的是,使用解析后的 XC 和 iNat 数据并没有改善我的模型,但给 @vialactea 带来了轻微的性能提升。
关于主要训练数据就是这样了。

5 秒的高光时刻

我们在 5 秒和随机选择的片段上训练模型。根据去年的经验,我们最初尝试了三种选择 5 秒片段的方法:从整个音频中随机选取 5 秒,从前 7 秒中随机选取 5 秒,以及从前 7 秒或后 7 秒中随机选取 5 秒。后两种方法的理由是,录音师通常会在动物发声时开始录音,在动物停止发声时停止录音。这将有助于模型避免误报。7 秒只是为了增加多样性。

在初步测试中,最后一种方法提供了更好的结果。然而,有些物种只有很少的音频——有些情况下只有 2 个——我们担心过拟合。因此,我们决定检查这些物种的音频,以手动识别发声部分。以下图表说明了我们发现的三种情况:

  • 带有外来语音的发声。所谓外来语音,是指解释录音的语音,没有发声痕迹,甚至与发声部分的背景噪声也不同。在图表中,外来语音发生在发声之前和之后。
    带有外来语音的频谱图
  • 与动物发声重叠的语音。在这种情况下,语音可以被视为背景噪声。在图表中,这发生在第 57 秒到 142 秒之间。最后 8 秒是外来语音。
    重叠语音的频谱图
  • 带有静默期的发声,即动物不发声的时候。
    带有静默期的频谱图
    避免误报一定是好事,对吧?嗯,不!跳过没有发声的随机 5 秒时期实际上降低了排行榜(LB)分数。为什么?是我们试图识别发声时不自量力了吗?是否有一些音频中的东西我们无法检测到但模型可以?也许那些误报通过防止模型对少数可用音频过拟合来帮助了泛化。我们最终要么使用整个音频,要么仅避免手动或自动识别的外来语音部分。看来名声与误报的关系比与我们的直觉更多。也许 AI 变得太擅长模仿生物智能,就像人一样,这些模型也developed 了对虚假东西的品味。

验证集是你所需但无法拥有的

不幸的是,这是所有 BirdCLEF 竞赛的真实故事。我们探索了一系列验证策略。每个策略的核心在于按 primary_label 分层并按 author 分组。之后,我们必须弄清楚如何处理采样不足的物种。所以,我们尝试了:

  1. 向每个验证折添加至少一个每个类别的样本,即使这引入了微小的数据泄露。我们还在每个折内引入了对采样不足物种的评分。
  2. 使用第一种方法,但从相应的训练折中移除添加的样本。
  3. 将所有采样不足的物种添加到训练折中,并从相应的验证折中移除它们。这允许模型看到更多采样不足物种的示例,并减少小类别验证分数中的噪声,但代价是失去对这些物种的验证反馈。

我们主要采用了第一种和第三种策略。
当然,最有趣的问题是:验证分数与我们的公共排行榜分数相关吗?经过一些重大改进后,我们看到两个指标都有显著的积极变化,但当在 ~1% AUC 范围内调整事物时,相关性几乎不存在。
LB 相关性图表
正如你在图中看到的,当我们突破 0.9 公共里程碑时,相关性决定休息了。

建模,老实说,是这里最无用的部分之一

我们遵循了以往鸟类和音频竞赛的良好传统,使用了 Spec → 2D CNN 方法。但特别是今年,骨干网络确实起了作用!我们尝试了 ConvNeXt 系列,它在 2023 年 showed 不错的结果,但这次失败了——同时也引入了非常大的延迟。ResNeXt 系列表现也不好。接下来的编码器成为了我们的最爱:

  • tf_efficientnetv2_s
  • eca_nfnet_l0

在某些设置中,EfficientNet 显示了更好的结果,而在其他设置中,nfnet_l0 占据了上风。此外,两者都非常适合集成。

关于分类头,事情很标准:我使用了 SED 头,而 @vialactea 使用了多层感知机。

训练到极致

两个骨干网络的共同部分:

  • 50 个 epoch
  • Batch size: 64。我们在竞赛的最后几天尝试了更大的 size,但不幸的是,没有可用的提交来测试它们。
  • 调度器:半个余弦周期
  • Focal + BCE loss
  • 标签平滑 0.005

优化器选择在骨干网络之间有所不同:

  • eca_nfnet_l0: RAdam,学习率 1e-4
  • tf_efficientnetv2_s: AdamW,学习率 1e-4,epsilon 1e-8,betas (0.9, 0.999)

我认为这里最重要的部分是平衡策略。我们探索了几种,并在最终集成中使用了不同的策略:

  • Balanced (平衡)
  • Squared (平方)
sample_weights = (
    all_primary_labels.value_counts() / 
    all_primary_labels.value_counts().sum()
) ** (-0.5)
  • Upsampling (上采样):重复来自最采样不足类别的样本,直到每个类别达到预定义的数量(例如,所有少于 100 个样本的类别都上采样到 100 个)。

我们最终使用了 Balanced 策略和 Balanced + Upsampling 的组合。

现在我们来到secret sauce 的第一个成分——预训练!

让我给你讲一个关于....预训练的故事

从最早的实验开始,@vialactea 在去年预训练的骨干网络上进行微调取得了非常好的结果。一般概念很简单:

  1. 下载巨大的 Xeno-Canto 数据集,排除包含今年物种的录音以避免数据泄露
  2. 过滤掉采样不足的物种,以避免在预训练期间使分类问题过于复杂——这导致大约 7,400–7,800 个物种
  3. 训练,训练,训练!

挑选正确的 checkpoint 是一门单独的艺术。我们尝试使用最后一个、最好的以及前 3 个最好的平均值。后两种方法效果最好。
仅使用预训练的骨干网络并丢弃分类头。
最初,我尝试仅在过去的竞赛数据上进行预训练——失败了。那个初始化使事情变得更糟。然后我采用了 @vialactea 的方法并确认它是一个杀手级功能:分数从 0.83–0.84 跳到了 0.86–0.87

在去年预训练模型成功之后,我们决定在新鲜的巨大 Xeno-Canto 快照上训练新的模型。不幸的是,进展不如预期:它们的表现与 2024 预训练模型相当或略差。尽管如此,一个 eca_nfnet_l0 checkpoint 显示出希望并被选入最终集成。

预训练后,我们在主要数据集上进行了微调,没有使用诸如骨干网络和头部的差异学习率等复杂技术。

数据增强

这里我完全参考 2023 年的 write-up,因为我们使用了完全相同的设置。我们尝试关闭 RandomFiltering 和/或 SpecAug,这导致了稍好的 CV 分数但更差的 LB。所以决定已定:不要让模型的生活太轻松——机器人应该工作!

释放克拉肯——伪标签时间!

我们使用了以下算法:

  1. 使用我们最好的集成以“提交”格式(5 秒窗口)预测所有 train_soundscapes
  2. 仅选择自信的片段——我们只保留最大概率大于 0.5 的片段。
  3. 使用软标签——不要应用阈值。这可以被视为集成蒸馏到单个模型的一种形式,同时也适应 soundscapes 的噪声和目标分布。
    重要:将低概率修剪为零——我们将所有低于 0.1 的概率设置为零。这是去除噪声、不自信标签的关键步骤。

伪代码看起来像这样:

…
# primary_label_prob 包含 5 秒块中的最大概率
soundscape_df = soundscape_df[soundscape_df["primary_label_prob"] > 0.5]
soundscape_df[scored_species < 0.1] = 0
…

下一步是正确采样。为了避免破坏(进一步)已经偏斜的数据分布并更好地控制采样伪标签的数量,我们使用了以下策略:

  1. 根据原始采样策略进行采样。
  2. 检查样本中的类别是否存在于 soundscape_df 中。如果是,则以某种概率(在我们的例子中为 0.4)选择它代替当前样本,并将硬标签替换为伪软标签向量。

第一次伪迭代将我们的分数从 0.86–0.87 提高到了 0.89–0.895。但如前所述——只有第一次...

下一个明显的步骤?堆叠更多的伪迭代!所以,我们使用在第一次伪迭代样本上训练的模型进行预测,这次使用未通过最大概率阈值的 soundscapes。这将分数提升到了 0.90–0.91 范围。

不幸的是,第三次迭代没有带来明显的收益,所以我们稍微改变了策略。主要问题是我们无法重新预测前一次迭代的 soundscapes,因为存在数据泄露。但是如果我们把 soundscapes 分成折呢?现在我们可以用“伪训练”模型以 OOF(折外)模式预测它们。遗憾的是,重复这种方法两次并没有将分数提升到第二次迭代以上。然而,它给了我们以略有不同方式生成的伪标签——然后我们将它们与之前的伪标签结合,形成疯狂的伪混合。基于 tf_efficientnetv2_s 的模型在这个混合上训练成为了我们的顶级独立模型,公共得分 0.917,私有得分 0.91。

伪迭代 选定文件数量
1 4430
2 1483
3 1437

添加一点后处理

与我们 2023 年的解决方案相反,今年我们决定严重过拟合。当然不是真的——但我们确实基于最佳验证 ROC AUC 选择了 checkpoints。我们尝试使用最后一个以及前 3 个最佳 checkpoints 的平均值,但这些通常在排行榜上表现较差。幸运的是,由于需要预测的文件较少,我们不太担心, simply 提交了每个实验的所有 5 折模型。

我们使用了一个简单的后处理步骤:对于每个音频文件,我们将所有块级预测乘以该文件中每个鸟类类别的最高概率。这会提升 consistently 强的预测并抑制较弱的预测,而不影响文件内排名(因为所有块都同等缩放)。然而,它改变了文件间的全局排名——例如,强文件(均值 = 0.9)中的 0.2 块变为 0.18,而弱文件(均值 = 0.4)中的 0.2 块变为 0.08。这 consistently 改善了结果,通常对于较弱的模型提高 0.01,对于较强的模型提高 0.005 左右。后处理函数的代码:

def postprocessing(input_df, top=1):
    only_probs = input_df.iloc[:, 1:].values
    N, F = only_probs.shape
    only_probs = only_probs.reshape((N//12, 12, F))
    mean_ = np.mean(np.sort(only_probs, axis=1)[:, -top:], axis=1, keepdims=True)
    only_probs *= mean_
    input_df.iloc[:, 1:] = only_probs.reshape((N, F))
    return input_df

我们在推理期间独立预测每个 5 秒块。我们也尝试了 previous TOP 4 团队 使用的重叠窗口方法(让我们遵循他们的命名称之为 TTA)。它在公共 LB 上表现良好,将最佳实验的分数从 0.917 提升到公共 0.922,从私有 0.91 提升到 0.918。不幸的是,它在公共排行榜上与后处理配合不佳。

最后...

我们的最终提交包含 3 个模型:

  1. tf_efficientnetv2_s 在我的 pipeline 上训练,使用 2024 预训练 checkpoint 进行 2 次伪迭代。在验证分割 #3 上训练,使用 Balanced 采样策略。
  2. eca_nfnet_l0 在我的 pipeline 上训练,使用 2025 预训练 checkpoint 进行 3 次伪迭代。在验证分割 #1 上训练,使用 Balanced + Upsampling 采样策略。
  3. tf_efficientnetv2_s 使用 @vialactea 的 pipeline 训练,使用 2024 预训练 checkpoint 进行 1 次伪迭代。在验证分割 #3 上训练,使用 Balanced 采样策略。

所有这些都由后处理支持,并且没有 TTA 预测。

一些失败尝试

  • 在主要训练数据的软标签上训练。这似乎是一个非常合乎逻辑的想法,但在深度学习中,正如通常那样——你永远不知道什么会起作用。
  • 使用最新 Xeno Canto 片段的预训练权重
  • 使用额外的 iNaturalist 或 XC 数据
  • 额外的增强,如 Time Flip

速览总结

强者将应付我们的长读,但对于其他人——一个简短的消融表。

改进 公共分数
Baseline 0.83-0.84
+ 预训练 0.86-0.87
+ 伪迭代 1 0.89-0.895
+ 伪迭代 2-3 0.9-0.91
+ TTA 0.922
后处理 +0.005-0.01

结语

我希望你在阅读时没有睡着。首先,我要祝贺 @vialactea 达到 Grandmaster 排名!很荣幸能与你一起参加这次竞赛!

我要感谢整个 Kaggle 社区,祝贺所有参与者和获胜者。特别感谢康奈尔鸟类学实验室、LifeCLEF、Google Research、Xeno-canto、@stefankahl、@tomdenton、@holgerklinck 和 @avocadomastermind。你们都在讨论中非常活跃,分享了数据集和有趣的材料,回答了所有问题,当然,还准备了这么酷的竞赛!

同比赛其他方案