654. BirdCLEF+ 2025 | birdclef-2025
感谢主办方和本次竞赛的每一位参与者!还要感谢我的伟大队友 @yuanzhezhou!我从这次有趣的比赛中学到了很多,很高兴能获得我的第一枚金牌。
我们的解决方案如下。
仅使用 2025 年的 train_audio 和 train_soundscapes。
我们移除了音频中 50% 的人声。因为我们发现移除所有人声会降低模型性能。
在我们的实验中,RMS 采样优于随机采样。
我们使用了 2023 年第 2 名解决方案 中开放的 sed 模型。感谢 @honglihang 提供的出色模型,我只需要修改一些配置和代码,就得到了一个在 LB 上得分 0.850+ 的单模型。
此外,我们还使用了公开 Notebook 中的 cnn 模型作为我们的第一阶段模型。
对于 sed 模型,我们在训练中使用 FocalBCE 损失。
对于 cnn 模型,我们使用 CE+BCE 损失。在训练早期,我们使用 ce 损失可以提高收敛速度,在训练后期使用 bce 损失。
对于原始信号:
对于 Mel 频谱图:
target_duration = 5
img_size = 384
SR = 32000
n_fft = 2048
n_mels = 256
f_min = 20
f_max = 16000
hop_length = target_duration * SR // (img_size - 1)
melspec_transform = torchaudio.transforms.MelSpectrogram(
sample_rate=SR,
hop_length=hop_length,
n_mels=n_mels,
f_min=f_min,
f_max=f_max,
n_fft=n_fft,
pad_mode="constant",
norm="slaney",
onesided=True,
mel_scale="htk",
)
db_transform = torchaudio.transforms.AmplitudeToDB(
stype="power", top_db=80
)
# 对于 cnn
SR = 32000
n_fft = 2048
n_mels = 128
f_min = 20
f_max = 14000
hop_length = 1024
在训练中应用 ema 使我的模型更加鲁棒,并缩小了不同 epoch 之间的差距。
对于 sed 模型,我们使用 efficientnetv2_b3, eca_nfet_l0 和 seresnext26t,并在 10 秒片段上进行训练。对于 cnn 模型,我们使用 efficientnet_b0,并在 5 秒片段上进行训练。
通过混合这些模型,我们可以得到一个在 LB 上得分为 0.888 的 sed 模型和一个在 LB 上得分为 0.840+ 的 cnn 模型。
我们使用我们的 sed、cnn 和公开 Notebook 中的模型来生成每 10 秒块伪标签。对于每个样本,我们仅使用前 10% 的类别作为软标签,其他类别设置为零。然后,我们使用相同的管道训练新的 sed 模型。在这个阶段,我们选择 efficientnetv2_b3 和 seresnext26t 作为我们最终模型的骨干网络。
通过使用伪标签,我们在 LB 上获得了 0.02+ 的提升。
我们将 10 秒块放入模型中,并使用 2 秒作为窗口长度来应用 TTA。您可以在我的推理 Notebook 中查看更多细节。所有模型首先都转换为 onnx 格式。
我们没有太多时间调整集成和后处理策略,所以我们只是使用加权平均来集成模型。
对于后处理:
我们注意到随机种子对结果有相对巨大的影响。所以我们集成了具有不同种子但相同管道的模型。
| 骨干网络 | 种子 | Public | Private |
|---|---|---|---|
| efficientnetv2_b3 ① | 42 | 0.903 | 0.913 |
| efficientnetv2_b3 ② | 3407 | 0.904 | 0.915 |
| efficientnetv2_b3 ③ | 2025 | 0.896 | 0.915 |
| seresnext26t ④ | 42 | 0.899 | 0.908 |
通过集成 ①、②、④,我们在 LB 上得到 0.913,在 Private 上得到 0.921,我们选择它作为最终提交。
通过集成 ①、②、③,我们在 LB 上得到 0.913 (较低),在 Private 上得到 0.922,但我们错过了这次提交。
实际上,在我们最后一次提交机会中,我们尝试集成所有 4 个模型,它在十个 60 秒的声音景观上大约花费了 1 分 20 秒。看起来不会超时,但确实超时了。
但是,当我们在比赛结束后使用单线程而不是多线程再次提交时,它运行良好,并在 Private 上获得了 0.924。看来 onnx+ 多线程会导致 bug,这使我们错过了进入前 5 名的机会 :(
由于 LB 的不稳定性,我们遗憾地错过了一些在 LB 上无效但在 Private 上表现良好的想法。
窗口长度为 10 秒,步长为 3 秒,模型预测每个 60 秒的声音景观。然后,我们在每个声音景观上得到 17 个片段,并选择概率总和最高的前 3 个片段。
这个想法让我得到了一个在 LB 上得分为 0.891 但在 Private 上得分为 0.916 的模型。
这个想法来自 2024 年第 3 名解决方案。
def max_mean_post(sub_df, cfg):
print("Postprocessing submission predictions...")
sub_df.loc[:, cfg.bird_cols] = logit(sub_df.iloc[:, 1:].values)
sub_df["group"] = sub_df['row_id'].str.rsplit('_', n=1).str[0]
dfg_max = sub_df[["group"] + cfg.bird_cols].groupby('group').max().reset_index()
dfg_mean = sub_df[["group"] + cfg.bird_cols].groupby('group').mean().reset_index()
delta = dfg_mean[cfg.bird_cols].mean(1) - dfg_max[cfg.bird_cols].mean(1)
for c in cfg.bird_cols:
dfg_max[c] += delta
sub_df = sub_df.merge(dfg_max, how="left", on="group", suffixes=('', '_delta'))
for c in cfg.bird_cols:
sub_df[c] = expit((sub_df[c] + sub_df[c + "_delta"]) / 2)
return sub_df.loc[:, ['row_id']+cfg.bird_cols].reset_index(drop=True)
它在 LB 上降低了 0.007,但在 Private 上提升了 0.001~0.003。
感谢大家!还有很多想法我没有时间尝试。希望能在 BirdCLEF 2026 中验证它们!