aboutsummaryrefslogtreecommitdiffstats
path: root/.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md
diff options
context:
space:
mode:
authorsillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
committersillylaird <sillyfanboy@gmail.com>2026-09-03 00:33:59 +0000
commit898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch)
tree85c6ee5ad58b860144551184d4cf86b560c62b91 /.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md
downloadwww-main.tar.gz
www-main.zip
import live www.sillylaird.ca webrootHEADmain
Diffstat (limited to '.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md')
-rw-r--r--.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md1860
1 files changed, 1860 insertions, 0 deletions
diff --git a/.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md b/.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md
new file mode 100644
index 0000000..003a556
--- /dev/null
+++ b/.agents/skills/kaggle-learner/references/knowledge/time-series/birdclef-2023.md
@@ -0,0 +1,1860 @@
+# BirdCLEF 2023
+> Last updated: 2026-01-25
+> Source count: 10+
+---
+
+### BirdCLEF 2023 - Bird Sound Identification (2023)
+
+**竞赛背景:**
+- **主办方**:Cornell Lab of Ornithology, LifeCLEF
+- **目标**:识别东非鸟类叫声,促进鸟类保护和生态监测
+- **应用场景**:自动化生物声学监测,替代人工识别
+- **社会意义**:大规模鸟类种群监测,生物多样性保护
+- **竞赛时间**:2023 年 3-5 月
+- **参赛队伍**:1,189 支团队
+
+**任务描述:**
+从肯尼亚 soundscape 音频中分类 264 种鸟类叫声:
+- **多标签分类**:一个音频可能包含多种鸟类
+- **评估指标**:**Macro-averaged ROC-AUC**(所有类别的平均)
+- 需要预测所有 264 个类别的概率
+- 提交格式:row_id × 264物种的概率矩阵
+
+**数据集规模:**
+- 训练数据:~20,000 个标注样本(5 秒片段)
+- 测试数据:未标注的 soundscape 音频(需 5 秒滑动窗口预测)
+- 音频长度:随机长度(5 秒到数分钟)
+- 采样率:通常为 32 kHz
+- 物种数量:264 种东非鸟类
+
+**数据特点:**
+1. **类别不平衡**:某些鸟类样本数 < 10,某些 > 1000
+2. **混合叫声**:一个音频可能包含多种鸟类
+3. **背景噪声**:风声、雨声、人声、昆虫声等环境噪声
+4. **未标注数据**:大量未标注 soundscape 可用于伪标签
+5. **领域偏移**:训练数据(哥伦比亚)与测试数据(肯尼亚)存在分布差异
+
+**竞赛约束:**
+- **推理限制**:仅 CPU,推理时间限制
+- 需要优化推理速度,不能使用太大模型
+- 提交文件大小限制
+
+**前排方案排名:**
+| 排名 | 团队/个人 | Private LB | 关键技术 |
+|------|----------|------------|----------|
+| **1st** | Volodymyr Sydorskyi | **0.76392** | Correct Data is All You Need - 数据清洗 + 外部数据 + 模型集成 |
+| **2nd** | Griffith | ~0.75+ | SED + CNN with 7 models ensemble |
+| **3rd** | ADSR | ~0.75 | SED with attention on Mel frequency bands |
+| **4th** | ATFujita | 0.74424 | Knowledge Distillation Is All You Need - 知识蒸馏 + Xeno-Canto |
+| **5th** | Yevhenii Maslov | ~0.74 | 外部数据 + 预训练 + 集成 |
+
+**技术演进(与后续版本对比):**
+| 技术点 | BirdCLEF 2023 | BirdCLEF 2024 | BirdCLEF+ 2025 |
+|--------|---------------|---------------|-----------------|
+| **物种数量** | 264 种 | 182 种 | 206 种(多分类群)|
+| **外部数据** | Xeno-Canto 重要 | 不用外部数据更优 | Xeno-Canto 预训练重要 |
+| **模型架构** | EfficientNetV2 + SED | EfficientNet B0 + RegNetY | EfficientNet + ViT |
+| **损失函数** | BCE + FocalLoss | CE Loss | BCE Loss |
+| **伪标签** | 高低阈值筛选 | Google Classifier 预标注 | Noisy Student |
+| **推理优化** | PyTorch | OpenVINO | OpenVINO |
+
+---
+
+## Competition Brief(竞赛简介)
+
+### 竞赛概述
+
+BirdCLEF 2023 是 Kaggle 上举办的鸟类声音识别竞赛,目标是从东非肯尼亚的 soundscape 音频中自动识别鸟类物种。该竞赛是 BirdCLEF 系列的 2023 年版本,属于时序音频分类任务。
+
+### 关键挑战
+
+1. **长尾分布**:264 个物种的样本数量极不均衡
+2. **领域偏移**:训练数据与测试数据来自不同地区
+3. **背景噪声**:实际环境中的各种噪声干扰
+4. **弱监督学习**:大量未标注 soundscape 数据需要利用
+5. **计算限制**:CPU 推理限制,需要优化推理速度
+
+### 评估机制
+
+- **指标**:Macro-averaged ROC-AUC
+- **评估方式**:每个类别独立计算 AUC,然后取平均
+- **提交格式**:CSV 文件,包含 row_id 和 264 个物种的概率列
+- **后处理**:允许基于时间和空间一致性的后处理
+
+---
+
+## 前排方案详细技术分析
+
+### 1st Place - Volodymyr Sydorskyi (Volodymyr)
+
+**最终成绩**:0.76392(Private LB)
+
+**核心策略**:Correct Data is All You Need
+
+**关键技术**:
+
+1. **数据清洗和质量控制**
+ - 严格的音频质量筛选
+ - 基于信噪比的过滤
+ - 去除低质量标注样本
+ - 时间戳验证和清洗
+
+2. **外部数据策略**
+ - Xeno-Canto 数据集成
+ - 跨年度数据利用(2021/2022 竞赛数据)
+ - 领域自适应技术
+ - 数据重采样策略
+
+3. **模型架构**
+ - EfficientNetV2 系列作为 backbone
+ - SED (Sound Event Detection) 框架
+ - 多尺度特征提取
+ - 注意力机制集成
+
+4. **训练策略**
+ - 两阶段训练:预训练 + 微调
+ - Focal Loss 处理类别不平衡
+ - 混合精度训练
+ - 梯度累积
+
+5. **集成策略**
+ - 多模型集成(不同 backbone 和配置)
+ - Checkpoint averaging
+ - 时序平滑后处理
+ - 基于物种出现时间的后处理
+
+**实现细节**:
+- 使用 EfficientNetV2-s 和 EfficientNetV2-m
+- 7+ 模型集成
+- Mel-spectrogram 参数:n_mels=128, fmin=64, fmax=16000
+- 数据增强:SpecAugment + MixUp
+- 推理优化:ONNX + 多线程
+
+**代码仓库**:
+- GitHub: [VSydorskyy/BirdCLEF_2023_1st_place](https://github.com/VSydorskyy/BirdCLEF_2023_1st_place)
+- Kaggle Writeup: [1st place solution: Correct Data is All You Need](https://www.kaggle.com/competitions/birdclef-2023/writeups/volodymyr-1st-place-solution-correct-data-is-all-y)
+
+---
+
+### 2nd Place - Griffith
+
+**最终成绩**:~0.75+(Private LB)
+
+**核心策略**:SED + CNN with 7 models ensemble
+
+**关键技术**:
+
+1. **SED (Sound Event Detection) 框架**
+ - 基于 EfficientNetV2-s 的 SED 模型
+ - 强时间建模能力
+ - 音频事件检测与分类结合
+ - 时序一致性约束
+
+2. **7 模型集成策略**
+ - 不同 backbone:EfficientNetV2-s, ResNet, ConvNeXt
+ - 不同输入尺寸和配置
+ - 不同 Mel 参数组合
+ - 加权集成代替简单平均
+
+3. **数据增强**
+ - SpecAugment(时间/频率掩码)
+ - MixUp 数据混合
+ - 颜色噪声注入
+ - 音频速度和音调变化
+
+4. **损失函数**
+ - BCE Loss(Binary Cross Entropy)
+ - Focal Loss 处理类别不平衡
+ - Label Smoothing
+ - 辅助损失函数
+
+**实现细节**:
+- EfficientNetV2-s backbone
+- SED 框架 + 自定义 CNN
+- 7 个模型集成
+- Mel 参数:n_mels=128-256 不同配置
+- 数据增强:SpecAugment + MixUp + 颜色噪声
+- 推理优化:模型并行 + 批处理
+
+**代码仓库**:
+- GitHub: [LIHANG-HONG/birdclef2023-2nd-place-solution](https://github.com/LIHANG-HONG/birdclef2023-2nd-place-solution)
+- Kaggle Writeup: [2nd place solution: SED + CNN with 7 models ensemble](https://www.kaggle.com/competitions/birdclef-2023/writeups/griffith-2nd-place-solution-sed-cnn-with-7-models-)
+
+---
+
+### 3rd Place - ADSR
+
+**最终成绩**:~0.75(Private LB)
+
+**核心策略**:SED with attention on Mel frequency bands
+
+**关键技术**:
+
+1. **Mel 频域注意力机制**
+ - 在 Mel 频率维度上添加注意力
+ - 自适应频率加权
+ - 频带重要性学习
+ - 多尺度频谱分析
+
+2. **改进的 SED 框架**
+ - CNN + RNN 混合架构
+ - 双向 LSTM 时序建模
+ - CRF 层优化时序一致性
+ - 多任务学习
+
+3. **特征工程**
+ - 多尺度 Mel-spectrogram
+ - MFCC 特征
+ - 频谱对比度增强
+ - 时频域联合分析
+
+4. **训练策略**
+ - 课程学习(从简单到困难)
+ - 难样本挖掘
+ - 在线难样本挖掘(OHEM)
+ - 渐进式训练
+
+**实现细节**:
+- 改进的 SED 架构
+- Mel 频域注意力机制
+- 双向 LSTM 时序建模
+- 多任务学习框架
+- 课程学习策略
+
+**代码仓库**:
+- Kaggle Writeup: [3rd place solution: SED with attention on Mel frequency bands](https://www.kaggle.com/competitions/birdclef-2023/writeups/adsr-3rd-place-solution-sed-with-attention-on-mel-)
+
+---
+
+### 4th Place - ATFujita
+
+**最终成绩**:0.74424(Private LB)
+
+**核心策略**:Knowledge Distillation Is All You Need
+
+**关键技术**:
+
+1. **知识蒸馏(Knowledge Distillation)**
+ - 使用 Kaggle Models 的 bird-vocalization-classifier 作为教师模型
+ - 预计算教师模型预测
+ - 蒸馏损失:KL 散度 + 学生损失
+ - 温度参数调优
+
+2. **Xeno-Canto 数据集成**
+ - 收集额外 Xeno-Canto 数据
+ - 数据过滤和质量控制
+ - 领域自适应
+ - 数据重采样
+
+3. **预训练策略**
+ - 在 Xeno-Canto 上预训练
+ - 在竞赛数据上微调
+ - 渐进式解冻
+ - 学习率调度
+
+4. **集成策略**
+ - 4 个模型集成
+ - 不同 backbone
+ - Checkpoint averaging
+ - 时序平滑
+
+**实现细节**:
+- BaseModel + Knowledge Distillation
+- 4 个模型集成
+- Xeno-Canto 预训练
+- Mel 参数:n_mels=128, fmin=64, fmax=16000
+- 数据增强:标准 SpecAugment
+- 推理优化:模型量化
+
+**代码仓库**:
+- GitHub: [AtsunoriFujita/BirdCLEF-2023-Identify-bird-calls-in-soundscapes](https://github.com/AtsunoriFujita/BirdCLEF-2023-Identify-bird-calls-in-soundscapes)
+- Kaggle Writeup: [4th Place Solution: Knowledge Distillation Is All You Need](https://www.kaggle.com/competitions/birdclef-2023/writeups/atfujita-4th-place-solution-knowledge-distillation)
+
+**关键创新**:
+- 使用预训练的 bird-vocalization-classifier 作为教师模型
+- 蒸馏损失与标准损失的加权组合
+- 高效的伪标签生成
+- 领域自适应技术
+
+---
+
+### 5th Place - Yevhenii Maslov
+
+**最终成绩**:~0.74(Private LB)
+
+**核心策略**:外部数据 + 预训练 + 集成
+
+**关键技术**:
+
+1. **外部数据利用**
+ - 2023/2022/2021 竞赛数据
+ - Xeno-Canto 数据(2023 物种)
+ - 数据过滤和清洗
+ - 数据平衡策略
+
+2. **预训练和微调**
+ - 在外部数据上预训练
+ - 在竞赛数据上微调
+ - 分层学习率
+ - 渐进式训练
+
+3. **模型架构**
+ - EfficientNetV2 系列
+ - SED 框架
+ - 注意力机制
+ - 多尺度特征融合
+
+4. **推理优化**
+ - 模型量化(INT8)
+ - 多线程推理
+ - 批处理优化
+ - ONNX 导出
+
+**实现细节**:
+- EfficientNetV2 backbone
+- SED 框架
+- 外部数据预训练
+- 5+ 模型集成
+- Mel 参数:标准配置
+- 推理优化:量化 + 多线程
+
+**代码仓库**:
+- GitHub: [yevmaslov](https://github.com/yevmaslov)
+- Kaggle Writeup: [5th place solution](https://www.kaggle.com/competitions/birdclef-2023/writeups/yevhenii-maslov-5th-place-solution)
+
+---
+
+### 8th Place - FURU-NAG
+
+**最终成绩**:~0.73(Private LB)
+
+**核心策略**:Implementing Multimodal Data Augmentation Methods
+
+**关键技术**:
+
+1. **多模态数据增强**
+ - 波形级增强:音调变化、时间拉伸、噪声注入
+ - 频谱级增强:SpecAugment、频率掩码、时间掩码
+ - 混合增强:MixUp、CutMix
+ - 自适应增强策略
+
+2. **防止过拟合**
+ - 现实音频组合
+ - 增强强度调度
+ - 在线增强
+ - 增强多样性
+
+3. **预处理管道**
+ - 音频质量检查
+ - 噪声过滤
+ - 音频归一化
+ - 特征标准化
+
+**实现细节**:
+- 多模态数据增强管道
+- 防止过拟合的策略
+- 现实音频组合
+- 自适应增强
+
+**代码仓库**:
+- Kaggle Writeup: [8th Place Solution: Implementing Multimodal Data Augmentation Methods](https://www.kaggle.com/competitions/birdclef-2023/writeups/furu-nag-8th-place-solution-implementing-multimoda)
+
+---
+
+### 18th Place - SED with Attention
+
+**核心策略**:SED with attention
+
+**关键技术**:
+
+1. **注意力机制**
+ - 时间注意力
+ - 频率注意力
+ - 自注意力
+ - 交叉注意力
+
+2. **SED 框架改进**
+ - 改进的时序建模
+ - 多尺度特征提取
+ - 注意力加权
+ - 残差连接
+
+**代码仓库**:
+- Kaggle Writeup: [18th place solution: SED with attention](https://www.kaggle.com/competitions/birdclef-2023/writeups/18th-place-solution-sed-with-attention)
+
+---
+
+## Code Templates(代码模板)
+
+### 1. Mel-Spectrogram 特征提取
+
+```python
+import torch
+import torchaudio
+import torch.nn as nn
+import numpy as np
+import librosa
+
+class MelSpectrogramExtractor:
+ """BirdCLEF 2023 统一 Mel-Spectrogram 提取器"""
+
+ def __init__(
+ self,
+ sample_rate: int = 32000,
+ n_mels: int = 128,
+ n_fft: int = 2048,
+ hop_length: int = 512,
+ fmin: float = 64.0,
+ fmax: float = 16000.0,
+ power: float = 2.0,
+ normalize: bool = True,
+ ):
+ self.sample_rate = sample_rate
+ self.n_mels = n_mels
+ self.n_fft = n_fft
+ self.hop_length = hop_length
+ self.fmin = fmin
+ self.fmax = fmax
+
+ # 使用 torchaudio 的 MelSpectrogram
+ self.mel_transform = torchaudio.transforms.MelSpectrogram(
+ sample_rate=sample_rate,
+ n_fft=n_fft,
+ hop_length=hop_length,
+ n_mels=n_mels,
+ f_min=fmin,
+ f_max=fmax,
+ power=power,
+ normalized=normalize,
+ )
+
+ def extract(self, waveform: torch.Tensor) -> torch.Tensor:
+ """
+ 提取 mel-spectrogram
+
+ Args:
+ waveform: (num_samples,) 或 (batch, num_samples)
+
+ Returns:
+ mel_spec: (n_mels, time) 或 (batch, n_mels, time)
+ """
+ if waveform.dim() == 1:
+ waveform = waveform.unsqueeze(0)
+
+ mel_spec = self.mel_transform(waveform)
+
+ # 转换为对数尺度
+ mel_spec = torch.log(mel_spec + 1e-9)
+
+ return mel_spec
+
+ def extract_fixed_length(
+ self, waveform: torch.Tensor, target_length: int
+ ) -> torch.Tensor:
+ """
+ 提取固定长度的 mel-spectrogram(用于 5 秒音频)
+
+ Args:
+ waveform: (num_samples,)
+ target_length: 目标时间维度
+
+ Returns:
+ mel_spec: (n_mels, target_length)
+ """
+ mel_spec = self.extract(waveform).squeeze(0)
+
+ # 调整到固定长度
+ if mel_spec.shape[1] < target_length:
+ # 填充
+ pad_length = target_length - mel_spec.shape[1]
+ mel_spec = nn.functional.pad(mel_spec, (0, pad_length))
+ else:
+ # 裁剪(从中心)
+ start = (mel_spec.shape[1] - target_length) // 2
+ mel_spec = mel_spec[:, start:start + target_length]
+
+ return mel_spec
+
+
+# 常用配置(前排方案)
+CONFIGS = {
+ "config_128": { # EfficientNetV2-s 标准
+ "n_mels": 128,
+ "n_fft": 2048,
+ "hop_length": 512,
+ "fmin": 64.0,
+ "fmax": 16000.0,
+ },
+ "config_256": { # 高分辨率
+ "n_mels": 256,
+ "n_fft": 4096,
+ "hop_length": 1024,
+ "fmin": 64.0,
+ "fmax": 16000.0,
+ },
+}
+
+# 使用示例
+extractor = MelSpectrogramExtractor(**CONFIGS["config_128"])
+waveform, sr = torchaudio.load("audio.wav")
+if sr != 32000:
+ waveform = torchaudio.transforms.Resample(sr, 32000)(waveform)
+mel_spec = extractor.extract_fixed_length(waveform.squeeze(0), target_length=313) # 5秒 -> 313帧
+```
+
+### 2. SED 模型架构(2nd Place 风格)
+
+```python
+import torch
+import torch.nn as nn
+import timm
+
+class SEDModel(nn.Module):
+ """
+ Sound Event Detection 模型
+ 基于 2nd Place Griffith 的方案
+ """
+
+ def __init__(
+ self,
+ model_name: str = "tf_efficientnetv2_s",
+ num_classes: int = 264,
+ pretrained: bool = True,
+ in_channels: int = 1,
+ rnn_layers: int = 1,
+ rnn_hidden: int = 128,
+ ):
+ super().__init__()
+
+ # Backbone(EfficientNetV2)
+ self.backbone = timm.create_model(
+ model_name,
+ pretrained=pretrained,
+ in_chans=in_channels,
+ num_classes=0, # 移除分类头
+ global_pool="", # 移除全局池化
+ )
+
+ # 获取 backbone 特征维度
+ backbone_features = self.backbone.num_features
+
+ # RNN 层(时序建模)
+ self.rnn = nn.LSTM(
+ input_size=backbone_features,
+ hidden_size=rnn_hidden,
+ num_layers=rnn_layers,
+ batch_first=True,
+ bidirectional=True,
+ )
+
+ # 分类头
+ self.classifier = nn.Sequential(
+ nn.Linear(rnn_hidden * 2, rnn_hidden),
+ nn.ReLU(),
+ nn.Dropout(0.3),
+ nn.Linear(rnn_hidden, num_classes),
+ )
+
+ def forward(self, x, return_segmentwise=False):
+ """
+ Args:
+ x: (batch, channels, n_mels, time)
+ return_segmentwise: 是否返回分段预测
+
+ Returns:
+ logits: (batch, num_classes) 或 (batch, time, num_classes)
+ """
+ batch_size = x.size(0)
+
+ # Backbone 特征提取
+ # (batch, channels, n_mels, time) -> (batch, features, time')
+ features = self.backbone(x)
+
+ # 转置为 (batch, time', features)
+ features = features.permute(0, 2, 1)
+
+ # RNN 时序建模
+ # (batch, time', features) -> (batch, time', rnn_hidden * 2)
+ rnn_out, _ = self.rnn(features)
+
+ if return_segmentwise:
+ # 分段预测(每个时间步)
+ segmentwise_logits = self.classifier(rnn_out)
+ return segmentwise_logits
+ else:
+ # 全局预测(时间平均池化)
+ global_features = rnn_out.mean(dim=1) # (batch, rnn_hidden * 2)
+ logits = self.classifier(global_features)
+ return logits
+
+
+# 使用示例
+model = SEDModel(
+ model_name="tf_efficientnetv2_s",
+ num_classes=264,
+ pretrained=True,
+ in_channels=1,
+ rnn_layers=1,
+ rnn_hidden=128,
+)
+
+# 前向传播
+mel_spec = torch.randn(4, 1, 128, 313) # (batch, channels, n_mels, time)
+logits = model(mel_spec) # (batch, 264)
+segmentwise_logits = model(mel_spec, return_segmentwise=True) # (batch, time, 264)
+```
+
+### 3. 带 Mel 频域注意力的 SED 模型(3rd Place 风格)
+
+```python
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+import timm
+
+class MelFrequencyAttention(nn.Module):
+ """Mel 频域注意力机制(3rd Place ADSR)"""
+
+ def __init__(self, n_mels: int, reduction: int = 8):
+ super().__init__()
+ self.avg_pool = nn.AdaptiveAvgPool2d(1)
+ self.max_pool = nn.AdaptiveMaxPool2d(1)
+
+ self.fc = nn.Sequential(
+ nn.Linear(n_mels, n_mels // reduction, bias=False),
+ nn.ReLU(inplace=True),
+ nn.Linear(n_mels // reduction, n_mels, bias=False),
+ )
+ self.sigmoid = nn.Sigmoid()
+
+ def forward(self, x):
+ """
+ Args:
+ x: (batch, channels, n_mels, time)
+
+ Returns:
+ attention: (batch, channels, n_mels, 1)
+ """
+ # 全局平均池化和最大池化
+ avg_out = self.avg_pool(x).squeeze(-1).squeeze(-1) # (batch, channels)
+ max_out = self.max_pool(x).squeeze(-1).squeeze(-1) # (batch, channels)
+
+ # 通过 FC 层
+ avg_out = self.fc(avg_out)
+ max_out = self.fc(max_out)
+
+ # 合并并应用 sigmoid
+ attention = self.sigmoid(avg_out + max_out)
+ attention = attention.unsqueeze(-1).unsqueeze(-1) # (batch, channels, n_mels, 1)
+
+ return attention
+
+
+class SEDWithMelAttention(nn.Module):
+ """带 Mel 频域注意力的 SED 模型"""
+
+ def __init__(
+ self,
+ model_name: str = "tf_efficientnetv2_s",
+ num_classes: int = 264,
+ pretrained: bool = True,
+ n_mels: int = 128,
+ rnn_hidden: int = 128,
+ ):
+ super().__init__()
+
+ # Backbone
+ self.backbone = timm.create_model(
+ model_name,
+ pretrained=pretrained,
+ in_chans=1,
+ num_classes=0,
+ global_pool="",
+ )
+
+ backbone_features = self.backbone.num_features
+
+ # Mel 频域注意力
+ self.mel_attention = MelFrequencyAttention(n_mels=n_mels)
+
+ # RNN 层
+ self.rnn = nn.LSTM(
+ input_size=backbone_features,
+ hidden_size=rnn_hidden,
+ num_layers=1,
+ batch_first=True,
+ bidirectional=True,
+ )
+
+ # 分类头
+ self.classifier = nn.Sequential(
+ nn.Linear(rnn_hidden * 2, rnn_hidden),
+ nn.ReLU(),
+ nn.Dropout(0.3),
+ nn.Linear(rnn_hidden, num_classes),
+ )
+
+ def forward(self, x, return_segmentwise=False):
+ """
+ Args:
+ x: (batch, 1, n_mels, time)
+
+ Returns:
+ logits: (batch, num_classes)
+ """
+ # Backbone 特征
+ features = self.backbone(x) # (batch, features, time')
+
+ # 应用 Mel 频域注意力
+ mel_att = self.mel_attention(features) # (batch, features, n_mels, 1)
+ features = features * mel_att
+
+ # 转置
+ features = features.permute(0, 2, 1) # (batch, time', features)
+
+ # RNN
+ rnn_out, _ = self.rnn(features)
+
+ if return_segmentwise:
+ segmentwise_logits = self.classifier(rnn_out)
+ return segmentwise_logits
+ else:
+ global_features = rnn_out.mean(dim=1)
+ logits = self.classifier(global_features)
+ return logits
+
+
+# 使用示例
+model = SEDWithMelAttention(
+ model_name="tf_efficientnetv2_s",
+ num_classes=264,
+ pretrained=True,
+ n_mels=128,
+ rnn_hidden=128,
+)
+```
+
+### 4. 数据增强(8th Place 风格)
+
+```python
+import torch
+import torchaudio
+import numpy as np
+
+class AudioAugmentation:
+ """多模态音频增强(8th Place FURU-NAG)"""
+
+ def __init__(
+ self,
+ sample_rate: int = 32000,
+ apply_prob: float = 0.5,
+ ):
+ self.sample_rate = sample_rate
+ self.apply_prob = apply_prob
+
+ def __call__(self, waveform: torch.Tensor) -> torch.Tensor:
+ """应用随机增强"""
+ if torch.rand(1).item() > self.apply_prob:
+ return waveform
+
+ # 随机选择增强方法
+ augmentations = [
+ self._pitch_shift,
+ self._time_stretch,
+ self._add_noise,
+ self._gain,
+ ]
+
+ np.random.shuffle(augmentations)
+
+ # 应用 1-2 种增强
+ num_augment = np.random.randint(1, 3)
+ for aug in augmentations[:num_augment]:
+ waveform = aug(waveform)
+
+ return waveform
+
+ def _pitch_shift(self, waveform: torch.Tensor) -> torch.Tensor:
+ """音调变化"""
+ if torch.rand(1).item() > 0.5:
+ return waveform
+
+ n_steps = np.random.uniform(-2, 2) # 半音
+ waveform_np = waveform.numpy()
+
+ # 使用 librosa 进行音调变化
+ shifted = librosa.effects.pitch_shift(
+ waveform_np,
+ sr=self.sample_rate,
+ n_steps=n_steps,
+ )
+
+ return torch.from_numpy(shifted).float()
+
+ def _time_stretch(self, waveform: torch.Tensor) -> torch.Tensor:
+ """时间拉伸"""
+ if torch.rand(1).item() > 0.5:
+ return waveform
+
+ rate = np.random.uniform(0.8, 1.2)
+ waveform_np = waveform.numpy()
+
+ # 使用 librosa 进行时间拉伸
+ stretched = librosa.effects.time_stretch(
+ waveform_np,
+ rate=rate,
+ )
+
+ return torch.from_numpy(stretched).float()
+
+ def _add_noise(self, waveform: torch.Tensor) -> torch.Tensor:
+ """添加噪声"""
+ if torch.rand(1).item() > 0.5:
+ return waveform
+
+ snr = np.random.uniform(10, 30) # 信噪比
+ noise = torch.randn_like(waveform)
+
+ # 计算噪声功率
+ signal_power = waveform.mean() ** 2
+ noise_power = noise.mean() ** 2
+
+ # 调整噪声功率
+ noise = noise * torch.sqrt(signal_power / (noise_power * (10 ** (snr / 10))))
+
+ return waveform + noise
+
+ def _gain(self, waveform: torch.Tensor) -> torch.Tensor:
+ """增益调整"""
+ if torch.rand(1).item() > 0.5:
+ return waveform
+
+ gain = np.random.uniform(0.8, 1.2)
+ return waveform * gain
+
+
+class SpecAugment:
+ """SpecAugment 增强(频谱增强)"""
+
+ def __init__(
+ self,
+ time_mask_param: int = 50,
+ freq_mask_param: int = 16,
+ num_time_masks: int = 2,
+ num_freq_masks: int = 2,
+ apply_prob: float = 0.5,
+ ):
+ self.time_mask_param = time_mask_param
+ self.freq_mask_param = freq_mask_param
+ self.num_time_masks = num_time_masks
+ self.num_freq_masks = num_freq_masks
+ self.apply_prob = apply_prob
+
+ def __call__(self, spec: torch.Tensor) -> torch.Tensor:
+ """
+ Args:
+ spec: (channels, n_mels, time)
+
+ Returns:
+ augmented_spec: (channels, n_mels, time)
+ """
+ if torch.rand(1).item() > self.apply_prob:
+ return spec
+
+ # 时间掩码
+ for _ in range(self.num_time_masks):
+ t = np.random.randint(0, self.time_mask_param)
+ t0 = np.random.randint(0, max(1, spec.size(-1) - t))
+ spec[:, :, t0:t0 + t] = 0
+
+ # 频率掩码
+ for _ in range(self.num_freq_masks):
+ f = np.random.randint(0, self.freq_mask_param)
+ f0 = np.random.randint(0, max(1, spec.size(-2) - f))
+ spec[:, f0:f0 + f, :] = 0
+
+ return spec
+
+
+class MixUp:
+ """MixUp 数据增强"""
+
+ def __init__(self, alpha: float = 0.5, apply_prob: float = 0.5):
+ self.alpha = alpha
+ self.apply_prob = apply_prob
+
+ def __call__(
+ self,
+ mel_spec: torch.Tensor,
+ labels: torch.Tensor,
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ """
+ Args:
+ mel_spec: (batch, channels, n_mels, time)
+ labels: (batch, num_classes)
+
+ Returns:
+ mixed_mel, mixed_labels
+ """
+ if torch.rand(1).item() > self.apply_prob:
+ return mel_spec, labels
+
+ batch_size = mel_spec.size(0)
+
+ # 生成混合权重
+ lam = np.random.beta(self.alpha, self.alpha)
+
+ # 随机排列
+ index = torch.randperm(batch_size)
+
+ # 混合特征和标签
+ mixed_mel = lam * mel_spec + (1 - lam) * mel_spec[index]
+ mixed_labels = lam * labels + (1 - lam) * labels[index]
+
+ return mixed_mel, mixed_labels
+
+
+# 使用示例
+audio_aug = AudioAugmentation(sample_rate=32000, apply_prob=0.8)
+spec_aug = SpecAugment(
+ time_mask_param=50,
+ freq_mask_param=16,
+ num_time_masks=2,
+ num_freq_masks=2,
+ apply_prob=0.8,
+)
+mixup = MixUp(alpha=0.5, apply_prob=0.5)
+
+# 音频增强
+waveform = torchaudio.load("audio.wav")[0]
+augmented_waveform = audio_aug(waveform)
+
+# 频谱增强
+mel_spec = torch.randn(4, 1, 128, 313)
+augmented_spec = spec_aug(mel_spec)
+
+# MixUp
+labels = torch.randint(0, 2, (4, 264)).float()
+mixed_spec, mixed_labels = mixup(mel_spec, labels)
+```
+
+### 5. 损失函数
+
+```python
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+
+class FocalLoss(nn.Module):
+ """Focal Loss(处理类别不平衡)"""
+
+ def __init__(
+ self,
+ alpha: float = 0.25,
+ gamma: float = 2.0,
+ reduction: str = "mean",
+ ):
+ super().__init__()
+ self.alpha = alpha
+ self.gamma = gamma
+ self.reduction = reduction
+
+ def forward(self, inputs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
+ """
+ Args:
+ inputs: (batch, num_classes) - logits
+ targets: (batch, num_classes) - one-hot or multi-hot labels
+
+ Returns:
+ loss
+ """
+ bce_loss = F.binary_cross_entropy_with_logits(
+ inputs, targets, reduction="none"
+ )
+
+ pt = torch.exp(-bce_loss)
+ focal_loss = self.alpha * (1 - pt) ** self.gamma * bce_loss
+
+ if self.reduction == "mean":
+ return focal_loss.mean()
+ elif self.reduction == "sum":
+ return focal_loss.sum()
+ else:
+ return focal_loss
+
+
+class CombinedLoss(nn.Module):
+ """组合损失(BCE + Focal Loss)"""
+
+ def __init__(
+ self,
+ bce_weight: float = 0.5,
+ focal_weight: float = 0.5,
+ focal_alpha: float = 0.25,
+ focal_gamma: float = 2.0,
+ label_smoothing: float = 0.0,
+ ):
+ super().__init__()
+ self.bce_weight = bce_weight
+ self.focal_weight = focal_weight
+
+ self.focal_loss = FocalLoss(
+ alpha=focal_alpha,
+ gamma=focal_gamma,
+ )
+
+ self.label_smoothing = label_smoothing
+
+ def forward(
+ self,
+ inputs: torch.Tensor,
+ targets: torch.Tensor,
+ ) -> torch.Tensor:
+ """
+ Args:
+ inputs: (batch, num_classes) - logits
+ targets: (batch, num_classes) - multi-hot labels
+
+ Returns:
+ loss
+ """
+ # Label smoothing
+ if self.label_smoothing > 0:
+ targets = targets * (1 - self.label_smoothing) + \
+ self.label_smoothing / targets.size(-1)
+
+ # BCE Loss
+ bce_loss = F.binary_cross_entropy_with_logits(inputs, targets)
+
+ # Focal Loss
+ focal_loss = self.focal_loss(inputs, targets)
+
+ # 组合
+ loss = self.bce_weight * bce_loss + self.focal_weight * focal_loss
+
+ return loss
+
+
+class KnowledgeDistillationLoss(nn.Module):
+ """知识蒸馏损失(4th Place)"""
+
+ def __init__(
+ self,
+ temperature: float = 4.0,
+ alpha: float = 0.7, # 蒸馏损失权重
+ ):
+ super().__init__()
+ self.temperature = temperature
+ self.alpha = alpha
+
+ def forward(
+ self,
+ student_logits: torch.Tensor,
+ teacher_logits: torch.Tensor,
+ targets: torch.Tensor,
+ ) -> torch.Tensor:
+ """
+ Args:
+ student_logits: (batch, num_classes) - 学生模型预测
+ teacher_logits: (batch, num_classes) - 教师模型预测(预计算)
+ targets: (batch, num_classes) - 真实标签
+
+ Returns:
+ loss
+ """
+ # 蒸馏损失(KL 散度)
+ T = self.temperature
+
+ # Soft targets
+ soft_teacher = F.softmax(teacher_logits / T, dim=-1)
+ soft_student = F.log_softmax(student_logits / T, dim=-1)
+
+ distillation_loss = F.kl_div(
+ soft_student,
+ soft_teacher,
+ reduction="batchmean",
+ ) * (T ** 2)
+
+ # 学生损失(标准 BCE)
+ student_loss = F.binary_cross_entropy_with_logits(
+ student_logits,
+ targets,
+ )
+
+ # 组合
+ loss = self.alpha * distillation_loss + (1 - self.alpha) * student_loss
+
+ return loss
+
+
+# 使用示例
+criterion = CombinedLoss(
+ bce_weight=0.5,
+ focal_weight=0.5,
+ focal_alpha=0.25,
+ focal_gamma=2.0,
+ label_smoothing=0.1,
+)
+
+logits = torch.randn(4, 264)
+targets = torch.randint(0, 2, (4, 264)).float()
+
+loss = criterion(logits, targets)
+print(f"Combined Loss: {loss.item()}")
+
+# 知识蒸馏
+kd_criterion = KnowledgeDistillationLoss(
+ temperature=4.0,
+ alpha=0.7,
+)
+
+student_logits = torch.randn(4, 264)
+teacher_logits = torch.randn(4, 264) # 预计算的教师预测
+
+kd_loss = kd_criterion(student_logits, teacher_logits, targets)
+print(f"KD Loss: {kd_loss.item()}")
+```
+
+---
+
+## Best Practices(最佳实践)
+
+### 1. 数据处理最佳实践
+
+#### 1.1 音频质量筛选(1st Place)
+
+```python
+import librosa
+import numpy as np
+
+def calculate_snr(audio: np.ndarray, sample_rate: int) -> float:
+ """计算信噪比(SNR)"""
+ # 使用能量计算 SNR
+ frame_length = 2048
+ frames = librosa.util.frame(audio, frame_length=frame_length, hop_length=512)
+
+ # 计算每帧能量
+ energies = np.mean(frames ** 2, axis=0)
+
+ # 信号能量:高能量帧
+ signal_energy = np.percentile(energies, 90)
+ # 噪声能量:低能量帧
+ noise_energy = np.percentile(energies, 10)
+
+ snr = 10 * np.log10(signal_energy / (noise_energy + 1e-9))
+ return snr
+
+def filter_audio_by_quality(
+ audio_path: str,
+ min_snr: float = 10.0,
+ max_duration: float = 60.0,
+) -> bool:
+ """根据质量筛选音频"""
+ try:
+ audio, sr = librosa.load(audio_path, sr=32000)
+
+ # 检查 SNR
+ snr = calculate_snr(audio, sr)
+ if snr < min_snr:
+ return False
+
+ # 检查时长
+ duration = len(audio) / sr
+ if duration > max_duration:
+ return False
+
+ # 检查是否静音
+ rms = librosa.feature.rms(y=audio)[0]
+ if np.mean(rms) < 0.01:
+ return False
+
+ return True
+
+ except Exception as e:
+ print(f"Error loading {audio_path}: {e}")
+ return False
+
+
+# 使用示例
+is_good_quality = filter_audio_by_quality("audio.wav", min_snr=10.0)
+```
+
+#### 1.2 外部数据集成(4th/5th Place)
+
+```python
+from pathlib import Path
+import pandas as pd
+
+def load_external_data(
+ data_dir: str,
+ species_list: list[str],
+ min_samples_per_species: int = 5,
+) -> pd.DataFrame:
+ """加载外部数据(Xeno-Canto)"""
+ data_dir = Path(data_dir)
+
+ all_records = []
+
+ for species in species_list:
+ species_dir = data_dir / species
+ if not species_dir.exists():
+ continue
+
+ audio_files = list(species_dir.glob("*.wav")) + \
+ list(species_dir.glob("*.mp3"))
+
+ # 过滤样本数少的物种
+ if len(audio_files) < min_samples_per_species:
+ continue
+
+ for audio_file in audio_files:
+ all_records.append({
+ "filename": str(audio_file),
+ "species": species,
+ "source": "xeno_canto",
+ })
+
+ return pd.DataFrame(all_records)
+
+
+# 使用示例
+species_list = ["bird_a", "bird_b", "bird_c"]
+external_df = load_external_data(
+ "data/xeno_canto",
+ species_list,
+ min_samples_per_species=5,
+)
+```
+
+### 2. 训练策略最佳实践
+
+#### 2.1 两阶段训练(1st/4th Place)
+
+```python
+import torch
+import torch.nn as nn
+from torch.utils.data import DataLoader
+
+def two_stage_training(
+ model: nn.Module,
+ train_loader_external: DataLoader,
+ train_loader_competition: DataLoader,
+ val_loader: DataLoader,
+ num_epochs_stage1: int = 10,
+ num_epochs_stage2: int = 20,
+ lr_stage1: float = 1e-3,
+ lr_stage2: float = 1e-4,
+):
+ """两阶段训练:外部数据预训练 + 竞赛数据微调"""
+
+ # Stage 1: 在外部数据上预训练
+ print("Stage 1: Pre-training on external data")
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr_stage1)
+ criterion = nn.BCEWithLogitsLoss()
+
+ for epoch in range(num_epochs_stage1):
+ model.train()
+ for batch in train_loader_external:
+ mel_spec = batch["mel_spec"].cuda()
+ labels = batch["labels"].cuda()
+
+ # 前向传播
+ logits = model(mel_spec)
+ loss = criterion(logits, labels)
+
+ # 反向传播
+ optimizer.zero_grad()
+ loss.backward()
+ optimizer.step()
+
+ # 验证
+ val_loss = validate(model, val_loader, criterion)
+ print(f"Epoch {epoch+1}/{num_epochs_stage1}, Val Loss: {val_loss:.4f}")
+
+ # Stage 2: 在竞赛数据上微调
+ print("Stage 2: Fine-tuning on competition data")
+ optimizer = torch.optim.AdamW(model.parameters(), lr=lr_stage2)
+
+ for epoch in range(num_epochs_stage2):
+ model.train()
+ for batch in train_loader_competition:
+ mel_spec = batch["mel_spec"].cuda()
+ labels = batch["labels"].cuda()
+
+ # 前向传播
+ logits = model(mel_spec)
+ loss = criterion(logits, labels)
+
+ # 反向传播
+ optimizer.zero_grad()
+ loss.backward()
+ optimizer.step()
+
+ # 验证
+ val_loss = validate(model, val_loader, criterion)
+ print(f"Epoch {epoch+1}/{num_epochs_stage2}, Val Loss: {val_loss:.4f}")
+
+ return model
+
+
+def validate(model: nn.Module, val_loader: DataLoader, criterion: nn.Module):
+ """验证"""
+ model.eval()
+ total_loss = 0
+
+ with torch.no_grad():
+ for batch in val_loader:
+ mel_spec = batch["mel_spec"].cuda()
+ labels = batch["labels"].cuda()
+
+ logits = model(mel_spec)
+ loss = criterion(logits, labels)
+
+ total_loss += loss.item()
+
+ return total_loss / len(val_loader)
+```
+
+#### 2.2 Checkpoint Averaging(2nd/4th Place)
+
+```python
+import torch
+from pathlib import Path
+
+def average_checkpoints(
+ checkpoint_paths: list[str],
+ output_path: str,
+):
+ """平均多个 checkpoint"""
+ # 加载所有 checkpoint
+ checkpoints = []
+ for path in checkpoint_paths:
+ ckpt = torch.load(path, map_location="cpu")
+ checkpoints.append(ckpt)
+
+ # 获取第一个 checkpoint 的结构
+ avg_state_dict = checkpoints[0]["model_state_dict"].copy()
+
+ # 计算平均
+ for key in avg_state_dict.keys():
+ tensors = [ckpt["model_state_dict"][key] for ckpt in checkpoints]
+ avg_state_dict[key] = torch.stack(tensors).mean(dim=0)
+
+ # 保存
+ torch.save({
+ "model_state_dict": avg_state_dict,
+ "epoch": sum([ckpt["epoch"] for ckpt in checkpoints]) // len(checkpoints),
+ }, output_path)
+
+ print(f"Averaged checkpoint saved to {output_path}")
+
+
+# 使用示例
+checkpoint_dir = Path("checkpoints")
+checkpoint_paths = [
+ str(checkpoint_dir / "model_epoch_13.pt"),
+ str(checkpoint_dir / "model_epoch_15.pt"),
+ str(checkpoint_dir / "model_epoch_17.pt"),
+ str(checkpoint_dir / "model_epoch_19.pt"),
+ str(checkpoint_dir / "model_epoch_20.pt"),
+]
+
+average_checkpoints(
+ checkpoint_paths,
+ "checkpoints/model_averaged.pt",
+)
+```
+
+### 3. 推理优化最佳实践
+
+#### 3.1 模型量化(5th Place)
+
+```python
+import torch
+import torch.nn as nn
+
+def quantize_model(
+ model: nn.Module,
+ calibration_loader: DataLoader,
+):
+ """量化模型到 INT8"""
+ # 动态量化
+ quantized_model = torch.quantization.quantize_dynamic(
+ model,
+ {nn.Linear, nn.Conv2d},
+ dtype=torch.qint8,
+ )
+
+ # 校准(静态量化需要)
+ # quantized_model.eval()
+ # with torch.no_grad():
+ # for batch in calibration_loader:
+ # _ = quantized_model(batch["mel_spec"])
+
+ return quantized_model
+
+
+# 使用示例
+quantized_model = quantize_model(model, val_loader)
+torch.save(quantized_model.state_dict(), "model_quantized.pt")
+```
+
+#### 3.2 ONNX 导出和优化
+
+```python
+import torch
+import torch.onnx
+import onnxruntime as ort
+
+def export_to_onnx(
+ model: nn.Module,
+ output_path: str,
+ input_shape: tuple = (1, 1, 128, 313),
+ opset_version: int = 13,
+):
+ """导出模型到 ONNX"""
+ model.eval()
+
+ # 创建示例输入
+ dummy_input = torch.randn(*input_shape)
+
+ # 导出
+ torch.onnx.export(
+ model,
+ dummy_input,
+ output_path,
+ opset_version=opset_version,
+ input_names=["mel_spec"],
+ output_names=["logits"],
+ dynamic_axes={
+ "mel_spec": {0: "batch_size"},
+ "logits": {0: "batch_size"},
+ },
+ )
+
+ print(f"Model exported to {output_path}")
+
+ # 优化 ONNX 模型
+ sess_options = ort.SessionOptions()
+ sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
+
+ session = ort.InferenceSession(
+ output_path,
+ sess_options,
+ providers=["CPUExecutionProvider"],
+ )
+
+ return session
+
+
+# 使用示例
+onnx_session = export_to_onnx(
+ model,
+ "model.onnx",
+ input_shape=(1, 1, 128, 313),
+)
+
+# ONNX 推理
+def predict_onnx(session: ort.InferenceSession, mel_spec: np.ndarray):
+ """使用 ONNX Runtime 推理"""
+ inputs = {session.get_inputs()[0].name: mel_spec}
+ outputs = session.run(None, inputs)
+
+ return outputs[0]
+
+
+# 批量推理
+def batch_predict_onnx(
+ session: ort.InferenceSession,
+ mel_specs: np.ndarray,
+ batch_size: int = 32,
+):
+ """批量推理"""
+ predictions = []
+
+ for i in range(0, len(mel_specs), batch_size):
+ batch = mel_specs[i:i+batch_size]
+ batch_pred = predict_onnx(session, batch)
+ predictions.append(batch_pred)
+
+ return np.concatenate(predictions, axis=0)
+```
+
+### 4. 集成策略最佳实践
+
+#### 4.1 加权集成(2nd Place)
+
+```python
+import numpy as np
+from scipy.optimize import minimize
+
+def find_optimal_weights(
+ predictions: np.ndarray,
+ targets: np.ndarray,
+) -> np.ndarray:
+ """
+ 找到最优集成权重
+
+ Args:
+ predictions: (num_models, num_samples, num_classes)
+ targets: (num_samples, num_classes)
+
+ Returns:
+ weights: (num_models,)
+ """
+ num_models = predictions.shape[0]
+
+ def objective(weights):
+ # 加权平均
+ weighted_pred = np.average(predictions, axis=0, weights=weights)
+ # 计算 AUC(简化版)
+ auc = compute_auc(weighted_pred, targets)
+ return -auc # 最小化负 AUC
+
+ # 约束:权重和为 1
+ constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
+ bounds = [(0, 1) for _ in range(num_models)]
+
+ # 初始权重:平均
+ initial_weights = np.ones(num_models) / num_models
+
+ # 优化
+ result = minimize(
+ objective,
+ initial_weights,
+ method="SLSQP",
+ bounds=bounds,
+ constraints=constraints,
+ )
+
+ return result.x
+
+
+def compute_auc(predictions: np.ndarray, targets: np.ndarray) -> float:
+ """计算 AUC(简化版)"""
+ from sklearn.metrics import roc_auc_score
+ return roc_auc_score(targets, predictions, average="macro")
+
+
+# 使用示例
+# predictions: (num_models, num_samples, num_classes)
+predictions = np.random.rand(5, 1000, 264)
+targets = np.random.randint(0, 2, (1000, 264))
+
+optimal_weights = find_optimal_weights(predictions, targets)
+print(f"Optimal weights: {optimal_weights}")
+
+# 加权集成
+final_predictions = np.average(predictions, axis=0, weights=optimal_weights)
+```
+
+#### 4.2 Min/Max Ensemble(1st Place 风格)
+
+```python
+import numpy as np
+
+def min_ensemble(predictions: np.ndarray) -> np.ndarray:
+ """
+ Min 集成(降低不确定预测)
+
+ Args:
+ predictions: (num_models, num_samples, num_classes)
+
+ Returns:
+ ensemble: (num_samples, num_classes)
+ """
+ return np.min(predictions, axis=0)
+
+
+def max_ensemble(predictions: np.ndarray) -> np.ndarray:
+ """
+ Max 集成(增强高置信预测)
+
+ Args:
+ predictions: (num_models, num_samples, num_classes)
+
+ Returns:
+ ensemble: (num_samples, num_classes)
+ """
+ return np.max(predictions, axis=0)
+
+
+def rank_ensemble(
+ predictions: np.ndarray,
+ method: str = "geometric",
+) -> np.ndarray:
+ """
+ Rank 集成(基于排名的集成)
+
+ Args:
+ predictions: (num_models, num_samples, num_classes)
+ method: "geometric" or "arithmetic"
+
+ Returns:
+ ensemble: (num_samples, num_classes)
+ """
+ # 计算排名
+ ranks = np.zeros_like(predictions)
+ for i in range(predictions.shape[0]):
+ ranks[i] = scipy.stats.rankdata(predictions[i], axis=-1)
+
+ # 平均排名
+ if method == "geometric":
+ avg_ranks = np.exp(np.mean(np.log(ranks + 1), axis=0)) - 1
+ else: # arithmetic
+ avg_ranks = np.mean(ranks, axis=0)
+
+ # 将排名转回概率
+ ensemble = avg_ranks / avg_ranks.sum(axis=-1, keepdims=True)
+
+ return ensemble
+
+
+# 使用示例
+predictions = np.random.rand(5, 1000, 264)
+
+min_pred = min_ensemble(predictions)
+max_pred = max_ensemble(predictions)
+rank_pred = rank_ensemble(predictions, method="geometric")
+```
+
+### 5. 后处理最佳实践
+
+#### 5.1 时序平滑(2nd/3rd Place)
+
+```python
+import numpy as np
+from scipy.ndimage import gaussian_filter1d
+
+def temporal_smoothing(
+ predictions: np.ndarray,
+ sigma: float = 1.0,
+) -> np.ndarray:
+ """
+ 时序平滑(高斯滤波)
+
+ Args:
+ predictions: (num_samples, num_classes) - 按时间排序
+ sigma: 高斯核标准差
+
+ Returns:
+ smoothed: (num_samples, num_classes)
+ """
+ smoothed = np.zeros_like(predictions)
+
+ for i in range(predictions.shape[1]):
+ smoothed[:, i] = gaussian_filter1d(predictions[:, i], sigma=sigma)
+
+ return smoothed
+
+
+def neighbor_window_smoothing(
+ predictions: np.ndarray,
+ window_size: int = 5,
+ neighbor_weight: float = 0.5,
+) -> np.ndarray:
+ """
+ 邻居窗口平滑(2nd Place 风格)
+
+ Args:
+ predictions: (num_samples, num_classes)
+ window_size: 窗口大小(奇数)
+ neighbor_weight: 邻居权重
+
+ Returns:
+ smoothed: (num_samples, num_classes)
+ """
+ half_window = window_size // 2
+ smoothed = np.zeros_like(predictions)
+
+ for i in range(len(predictions)):
+ # 获取邻居窗口
+ start = max(0, i - half_window)
+ end = min(len(predictions), i + half_window + 1)
+
+ window = predictions[start:end]
+
+ # 中心样本权重为 1,邻居权重为 neighbor_weight
+ weights = np.ones(len(window))
+ weights[weights == 1] = neighbor_weight
+ weights[len(window) // 2] = 1.0
+
+ # 加权平均
+ smoothed[i] = np.average(window, axis=0, weights=weights)
+
+ return smoothed
+
+
+# 使用示例
+predictions = np.random.rand(100, 264) # 100 个时间步
+
+smoothed_gaussian = temporal_smoothing(predictions, sigma=1.5)
+smoothed_neighbor = neighbor_window_smoothing(
+ predictions,
+ window_size=5,
+ neighbor_weight=0.5,
+)
+```
+
+#### 5.2 基于物种时间的后处理
+
+```python
+import numpy as np
+import pandas as pd
+
+def species_time_filtering(
+ predictions: pd.DataFrame,
+ time_info: pd.DataFrame,
+ species_activity: dict,
+) -> pd.DataFrame:
+ """
+ 基于物种活动时间的后处理
+
+ Args:
+ predictions: (num_samples, num_species) - 包含 species columns
+ time_info: (num_samples,) - 包含 "time" column
+ species_activity: {species: {active_hours: [start, end]}}
+
+ Returns:
+ filtered_predictions
+ """
+ filtered = predictions.copy()
+
+ for species, activity in species_activity.items():
+ if species not in predictions.columns:
+ continue
+
+ active_hours = activity["active_hours"] # [start, end]
+
+ # 获取小时
+ hours = pd.to_datetime(time_info["time"]).dt.hour
+
+ # 在非活跃时间降低预测
+ mask = (hours < active_hours[0]) | (hours > active_hours[1])
+ filtered.loc[mask, species] *= 0.5
+
+ return filtered
+
+
+# 使用示例
+predictions_df = pd.DataFrame({
+ "bird_a": np.random.rand(100),
+ "bird_b": np.random.rand(100),
+})
+
+time_info_df = pd.DataFrame({
+ "time": pd.date_range("2023-01-01 00:00", periods=100, freq="5min"),
+})
+
+species_activity = {
+ "bird_a": {"active_hours": [6, 18]}, # 6:00-18:00 活跃
+ "bird_b": {"active_hours": [18, 6]}, # 夜间活跃
+}
+
+filtered_predictions = species_time_filtering(
+ predictions_df,
+ time_info_df,
+ species_activity,
+)
+```
+
+---
+
+## 关键技术创新总结
+
+### BirdCLEF 2023 vs 2024 vs 2025 对比
+
+| 维度 | BirdCLEF 2023 | BirdCLEF 2024 | BirdCLEF+ 2025 |
+|------|---------------|---------------|----------------|
+| **物种数量** | 264 种 | 182 种 | 206 种(多分类群)|
+| **评估指标** | Macro AUC-ROC | AUC-ROC | Multi-Label AUC-ROC |
+| **外部数据** | Xeno-Canto 重要 | 不用外部数据 | Xeno-Canto 预训练重要 |
+| **模型架构** | EfficientNetV2 + SED | EfficientNet B0 + RegNetY | EfficientNet + ViT |
+| **损失函数** | BCE + FocalLoss | CE Loss | BCE Loss |
+| **伪标签** | 高低阈值筛选 | Google Classifier 预标注 | Noisy Student |
+| **推理优化** | PyTorch | OpenVINO | OpenVINO |
+| **关键创新** | 数据清洗 + 知识蒸馏 | Statistics T 过滤 | 自蒸馏 + 幂次变换 |
+
+### BirdCLEF 2023 独特创新
+
+1. **数据清洗(1st Place)**
+ - 严格的音频质量筛选
+ - 基于信噪比的过滤
+ - 时间戳验证
+
+2. **知识蒸馏(4th Place)**
+ - 使用预训练 bird-vocalization-classifier
+ - 蒸馏损失 + 学生损失
+ - 温度参数调优
+
+3. **Mel 频域注意力(3rd Place)**
+ - 自适应频率加权
+ - 多尺度频谱分析
+ - 频带重要性学习
+
+4. **多模态增强(8th Place)**
+ - 波形级增强
+ - 频谱级增强
+ - 现实音频组合
+
+---
+
+## 参考资料
+
+### Kaggle Writeups
+
+1. **[1st place solution: Correct Data is All You Need](https://www.kaggle.com/competitions/birdclef-2023/writeups/volodymyr-1st-place-solution-correct-data-is-all-y)** - Volodymyr Sydorskyi
+2. **[2nd place solution: SED + CNN with 7 models ensemble](https://www.kaggle.com/competitions/birdclef-2023/writeups/griffith-2nd-place-solution-sed-cnn-with-7-models-)** - Griffith
+3. **[3rd place solution: SED with attention on Mel frequency bands](https://www.kaggle.com/competitions/birdclef-2023/writeups/adsr-3rd-place-solution-sed-with-attention-on-mel-)** - ADSR
+4. **[4th Place Solution: Knowledge Distillation Is All You Need](https://www.kaggle.com/competitions/birdclef-2023/writeups/atfujita-4th-place-solution-knowledge-distillation)** - ATFujita
+5. **[5th place solution](https://www.kaggle.com/competitions/birdclef-2023/writeups/yevhenii-maslov-5th-place-solution)** - Yevhenii Maslov
+6. **[8th Place Solution: Implementing Multimodal Data Augmentation Methods](https://www.kaggle.com/competitions/birdclef-2023/writeups/furu-nag-8th-place-solution-implementing-multimoda)** - FURU-NAG
+7. **[18th place solution: SED with attention](https://www.kaggle.com/competitions/birdclef-2023/writeups/18th-place-solution-sed-with-attention)**
+
+### GitHub Repositories
+
+1. **[VSydorskyy/BirdCLEF_2023_1st_place](https://github.com/VSydorskyy/BirdCLEF_2023_1st_place)** - 1st Place 代码
+2. **[LIHANG-HONG/birdclef2023-2nd-place-solution](https://github.com/LIHANG-HONG/birdclef2023-2nd-place-solution)** - 2nd Place 代码
+3. **[AtsunoriFujita/BirdCLEF-2023-Identify-bird-calls-in-soundscapes](https://github.com/AtsunoriFujita/BirdCLEF-2023-Identify-bird-calls-in-soundscapes)** - 4th Place 代码
+4. **[yevmaslov](https://github.com/yevmaslov)** - 5th Place 代码
+
+### 学术论文
+
+1. **[Overview of BirdCLEF 2023: Automated Bird Species Identification in Eastern Africa](https://hal.science/hal-05182512/document)** - 竞赛概述
+2. **[Acoustic Bird Species Recognition at BirdCLEF 2023](https://ceur-ws.org/Vol-3497/paper-172.pdf)** - 2nd Place 学术论文
+3. **[Bird Species Recognition using Convolutional Neural Networks with Attention on Frequency Bands](https://www.researchgate.net/publication/389264675_Bird_Species_Recognition_using_Convolutional_Neural_Networks_with_Attention_on_Frequency_Bands)**
+
+### 技术博客
+
+1. **[(Kaggle) BirdCLEF 2023 - 24th (top 2%) place solution](https://kozistr.tech/2023-05-26-birdcelf-2023/)** - 24th Place 详细方案
+2. **[763rd Place Solution for the BirdCLEF 2023 Competition](https://www.kaggle.com/competitions/birdclef-2023/discussion/451041)** - 讨论帖
+
+### 其他资源
+
+1. **[Leaderboard - BirdCLEF 2023](https://www.kaggle.com/competitions/birdclef-2023/leaderboard)** - 最终排行榜
+2. **[BirdCLEF 2023 Competition Page](https://www.kaggle.com/competitions/birdclef-2023)** - 竞赛主页
+3. **[Xeno-canto Extended Metadata for BirdCLEF2023](https://www.kaggle.com/datasets/mariotsaberlin/xeno-canto-extended-metadata-for-birdclef2023)** - Xeno-Canto 扩展数据集
+
+---
+
+## 总结
+
+BirdCLEF 2023 是一个专注于东非鸟类声音识别的竞赛,其关键特点包括:
+
+1. **数据质量是关键**(1st Place):严格的音频质量筛选比模型架构更重要
+2. **外部数据的战略使用**:Xeno-Canto 数据的合理集成和领域自适应
+3. **SED 框架的普及**:前排方案大多采用 SED(Sound Event Detection)框架
+4. **知识蒸馏的应用**(4th Place):使用预训练模型作为教师提升性能
+5. **注意力机制的优化**(3rd Place):Mel 频域注意力机制提升特征提取
+6. **多模态数据增强**(8th Place):波形级和频谱级的联合增强
+
+**与后续版本的主要区别**:
+- BirdCLEF 2023 更依赖外部数据(Xeno-Canto)
+- BirdCLEF 2024 强调不使用外部数据,关注数据清洗
+- BirdCLEF+ 2025 扩展到多分类群(鸟类、两栖、哺乳、昆虫)
+
+**技术演进趋势**:
+- 从 EfficientNetV2 → EfficientNet B0/RegNetY
+- 从 BCE+Focal Loss → CE Loss → BCE Loss
+- 从伪标签高低阈值 → Google Classifier 预标注 → Noisy Student
+- 从 PyTorch 推理 → OpenVINO 优化