Files
2026-05-30 16:22:29 +08:00

67 KiB
Raw Blame History

BirdCLEF+ 2025

Last updated: 2026-01-23 Source count: 1


Mel-Spectrogram 特征提取BirdCLEF+ 2025

基于前排方案,统一的 mel-spectrogram 提取流程:

import torch
import torchaudio
import torch.nn as nn
import numpy as np

class MelSpectrogramExtractor:
    """统一的 Mel-Spectrogram 提取器"""

    def __init__(
        self,
        sample_rate: int = 32000,
        n_mels: int = 128,
        n_fft: int = 2048,
        hop_length: int = 512,
        fmin: float = 0.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": {  # tf_efficientnet 系列
        "n_mels": 128,
        "n_fft": 2048,
        "hop_length": 512,
        "fmin": 0.0,
        "fmax": 16000.0,
    },
    "config_96": {  # 轻量级模型
        "n_mels": 96,
        "n_fft": 2048,
        "hop_length": 512,
        "fmin": 0.0,
        "fmax": 16000.0,
    },
    "config_256": {  # 高分辨率
        "n_mels": 256,
        "n_fft": 4096,
        "hop_length": 1024,
        "fmin": 0.0,
        "fmax": 16000.0,
    },
}

# 使用示例
extractor = MelSpectrogramExtractor(**CONFIGS["config_128"])
waveform, sr = torchaudio.load("audio.wav")
mel_spec = extractor.extract_fixed_length(waveform.squeeze(0), target_length=313)  # 5秒 -> 313帧

伪标签生成BirdCLEF+ 2025

import torch
import torch.nn as nn
import numpy as np
from pathlib import Path

class PseudoLabelGenerator:
    """伪标签生成器 - 基于前排方案"""

    def __init__(
        self,
        model: nn.Module,
        threshold: float = 0.4,
        use_segmentwise: bool = True,
        power_transform: float = 1.0,
    ):
        """
        Args:
            model: 训练好的模型
            threshold: 置信度阈值(前排方案使用 0.3-0.5
            use_segmentwise: 是否使用 segmentwise_logit更细粒度
            power_transform: 幂次变换参数1st Place 使用)
        """
        self.model = model
        self.model.eval()
        self.threshold = threshold
        self.use_segmentwise = use_segmentwise
        self.power_transform = power_transform

    @torch.no_grad()
    def generate_pseudo_labels(
        self,
        audio_path: str,
        segment_duration: int = 5,
        overlap: float = 0.5,
    ) -> list[dict]:
        """
        生成伪标签

        Returns:
            List of {"start": float, "end": float, "labels": np.ndarray}
        """
        # 加载音频
        waveform, sr = torchaudio.load(audio_path)

        # 分段处理
        samples_per_segment = int(segment_duration * sr)
        hop_length = int(samples_per_segment * (1 - overlap))

        pseudo_labels = []

        for start_idx in range(0, len(waveform) - samples_per_segment, hop_length):
            end_idx = start_idx + samples_per_segment
            segment = waveform[:, start_idx:end_idx]

            # 提取特征
            mel_spec = self.extract_mel(segment)

            # 模型预测
            if self.use_segmentwise:
                # segmentwise_logit: 更细粒度的预测
                logits = self.model(mel_spec, return_segmentwise=True)
                # 时间维度平均
                logits = logits.mean(dim=1)  # (batch, num_classes)
            else:
                logits = self.model(mel_spec)

            # Sigmoid 激活
            probs = torch.sigmoid(logits).squeeze(0).cpu().numpy()

            # 幂次变换1st Place 创新)
            if self.power_transform != 1.0:
                probs = np.power(probs, self.power_transform)

            # 高低阈值筛选10th Place 方法)
            mask = self._apply_threshold(probs)

            if mask.sum() > 0:
                pseudo_labels.append({
                    "start": start_idx / sr,
                    "end": end_idx / sr,
                    "labels": probs,
                    "mask": mask,
                })

        return pseudo_labels

    def _apply_threshold(self, probs: np.ndarray) -> np.ndarray:
        """应用高低阈值筛选"""
        # 高阈值:正样本
        high_threshold = 0.7
        # 低阈值:负样本
        low_threshold = 0.3

        mask = np.zeros_like(probs, dtype=bool)
        mask[probs >= high_threshold] = True  # 高置信度正样本
        mask[probs <= low_threshold] = True   # 低置信度负样本

        return mask

    def extract_mel(self, waveform: torch.Tensor) -> torch.Tensor:
        """提取 mel-spectrogram简化版本"""
        # 实际使用中应该与训练时的提取器一致
        pass


# 使用示例(前排方案风格)
generator = PseudoLabelGenerator(
    model=model,
    threshold=0.4,
    use_segmentwise=True,  # 6th Place 关键
    power_transform=1.5,   # 1st Place 幂次变换
)

pseudo_labels = generator.generate_pseudo_labels("train_soundscape_01.wav")

MixUp 数据增强BirdCLEF+ 2025

import torch
import torch.nn as nn
import numpy as np

class AudioMixUp:
    """音频 MixUp 增强 - 前排方案风格"""

    def __init__(
        self,
        alpha: float = 0.5,
        mixup_type: str = "hard",  # "hard" 或 "soft"
        probability: float = 0.5,
    ):
        """
        Args:
            alpha: Beta 分布参数
            mixup_type:
                - "soft": 标准混合标签MixUp
                - "hard": 硬混合标签8th Place 创新)
            probability: 应用 MixUp 的概率
        """
        self.alpha = alpha
        self.mixup_type = mixup_type
        self.probability = probability

    def __call__(
        self,
        batch: dict,
    ) -> dict:
        """
        应用 MixUp

        Args:
            batch: {"mel": (B, C, H, W), "labels": (B, num_classes)}

        Returns:
            Mixed batch
        """
        if torch.rand(1).item() > self.probability:
            return batch

        mel = batch["mel"]
        labels = batch["labels"]

        batch_size = mel.size(0)

        # 生成混合权重
        lam = np.random.beta(self.alpha, self.alpha)

        # 随机排列
        index = torch.randperm(batch_size)

        # 混合特征
        mixed_mel = lam * mel + (1 - lam) * mel[index]

        # 混合标签
        if self.mixup_type == "soft":
            # 标准 MixUp: 软标签混合
            mixed_labels = lam * labels + (1 - lam) * labels[index]
        elif self.mixup_type == "hard":
            # 硬 MixUp (8th Place): 混合标签的最大值
            mixed_labels = torch.maximum(labels, labels[index])
        else:
            raise ValueError(f"Unknown mixup_type: {self.mixup_type}")

        return {
            "mel": mixed_mel,
            "labels": mixed_labels,
            "lam": lam,  # 可能用于损失调整
        }


# Sumix 增强13th Place 使用)
class Sumix:
    """Sumix: 原始信号上的 MixUp"""

    def __init__(self, alpha: float = 0.5, probability: float = 1.0):
        self.alpha = alpha
        self.probability = probability

    def __call__(
        self,
        waveform: torch.Tensor,
        labels: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor]:
        """
        在原始波形上应用 Sumix

        Args:
            waveform: (batch, num_samples)
            labels: (batch, num_classes)

        Returns:
            Mixed waveform and labels
        """
        if torch.rand(1).item() > self.probability:
            return waveform, labels

        batch_size = waveform.size(0)
        lam = np.random.beta(self.alpha, self.alpha)
        index = torch.randperm(batch_size)

        # 混合波形
        mixed_waveform = lam * waveform + (1 - lam) * waveform[index]

        # 混合标签(最大值)
        mixed_labels = torch.maximum(labels, labels[index])

        return mixed_waveform, mixed_labels


# 使用示例
mixup = AudioMixUp(alpha=0.5, mixup_type="hard", probability=0.5)
sumix = Sumix(alpha=0.5, probability=1.0)

# 训练循环中
for batch in dataloader:
    # Sumix 在原始波形
    waveform, labels = sumix(batch["waveform"], batch["labels"])

    # 提取 mel-spectrogram
    mel = extract_mel(waveform)

    # MixUp 在 mel-spectrogram
    batch = mixup({"mel": mel, "labels": labels})

Soft AUC LossBirdCLEF+ 2025 - 4th Place

支持软标签的 AUC 损失函数:

import torch
import torch.nn as nn
import torch.nn.functional as F

class SoftAUCLoss(nn.Module):
    """
    Soft AUC Loss - 4th Place 创新

    支持 soft labels适用于知识蒸馏和半监督学习
    """

    def __init__(self, reduction: str = "mean"):
        super().__init__()
        self.reduction = reduction

    def forward(
        self,
        predictions: torch.Tensor,
        targets: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            predictions: (batch, num_classes) - 原始 logits
            targets: (batch, num_classes) - 软标签 [0, 1]

        Returns:
            AUC loss
        """
        # Sigmoid 激活
        probs = torch.sigmoid(predictions)

        # 计算 AUC loss
        # 对每个类别独立计算
        num_classes = predictions.size(1)
        losses = []

        for c in range(num_classes):
            # 当前类别的预测和目标
            prob_c = probs[:, c]
            target_c = targets[:, c]

            # 按目标值排序(软标签)
            sorted_indices = torch.argsort(target_c, descending=True)

            # 计算正负样本的得分差异
            # 对于软标签,我们需要加权处理
            positive_scores = prob_c[sorted_indices[:len(sorted_indices)//2]]
            negative_scores = prob_c[sorted_indices[len(sorted_indices)//2:]]

            # AUC 近似:正样本得分应该高于负样本
            # 使用 sigmoid 差异
            diff = positive_scores.unsqueeze(1) - negative_scores.unsqueeze(0)
            loss_c = torch.sigmoid(-diff).mean()

            losses.append(loss_c)

        losses = torch.stack(losses)

        if self.reduction == "mean":
            return losses.mean()
        elif self.reduction == "sum":
            return losses.sum()
        else:
            return losses


# 改进的 AUC Loss更稳定
class ImprovedAUCLoss(nn.Module):
    """
    改进的 AUC Loss - 更稳定且支持软标签
    """

    def __init__(self, margin: float = 1.0):
        super().__init__()
        self.margin = margin

    def forward(
        self,
        predictions: torch.Tensor,
        targets: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            predictions: (batch, num_classes)
            targets: (batch, num_classes) - 软标签
        """
        probs = torch.sigmoid(predictions)
        num_classes = predictions.size(1)

        losses = []
        for c in range(num_classes):
            prob_c = probs[:, c]
            target_c = targets[:, c]

            # 计算成对损失
            # 对于每个样本对 (i, j):
            # 如果 target_i > target_j则希望 prob_i > prob_j
            n = prob_c.size(0)
            if n < 2:
                continue

            # 创建样本对矩阵
            target_diff = target_c.unsqueeze(1) - target_c.unsqueeze(0)
            prob_diff = prob_c.unsqueeze(1) - prob_c.unsqueeze(0)

            # 只考虑 target_i > target_j 的对
            mask = target_diff > 0

            if mask.sum() > 0:
                # Hinge loss: max(0, margin - (prob_i - prob_j))
                loss_c = F.relu(self.margin - prob_diff)[mask].mean()
                losses.append(loss_c)

        if len(losses) == 0:
            return torch.tensor(0.0, device=predictions.device)

        return torch.stack(losses).mean()


# 使用示例
criterion = SoftAUCLoss(reduction="mean")

# 训练循环
for batch in dataloader:
    predictions = model(batch["mel"])

    # 支持软标签
    loss = criterion(predictions, batch["labels"])

    loss.backward()
    optimizer.step()

滑动窗口推理BirdCLEF+ 2025 - 1st Place

import torch
import torch.nn as nn
from scipy.ndimage import gaussian_filter1d

class SlidingWindowInference:
    """
    滑动窗口推理 - 1st Place 创新

    使用帧预测的平均值,而不是仅使用中心窗口的最大值
    """

    def __init__(
        self,
        model: nn.Module,
        window_size: int = 5,  # 秒
        hop_size: int = 5,     # 秒(步长)
        sample_rate: int = 32000,
        smoothing_sigma: float = 1.0,
    ):
        self.model = model
        self.model.eval()
        self.window_size = window_size
        self.hop_size = hop_size
        self.sample_rate = sample_rate
        self.smoothing_sigma = smoothing_sigma

    @torch.no_grad()
    def predict(
        self,
        audio_path: str,
    ) -> dict[str, float]:
        """
        对整个音频进行预测,返回 5 秒窗口的预测

        Returns:
            Dict of {row_id: {species_id: probability}}
        """
        # 加载音频
        waveform, sr = torchaudio.load(audio_path)

        # 计算窗口参数
        samples_per_window = int(self.window_size * sr)
        samples_per_hop = int(self.hop_size * sr)

        # 存储所有帧预测
        all_frame_predictions = []

        # 滑动窗口
        window_id = 0
        for start_idx in range(0, len(waveform) - samples_per_window, samples_per_hop):
            end_idx = start_idx + samples_per_window
            window = waveform[:, start_idx:end_idx]

            # 提取特征
            mel_spec = self.extract_mel(window)

            # 模型预测
            frame_output = self.model(mel_spec)

            # 如果是 SED 模型,可能有 clipwise 和 segmentwise 输出
            if isinstance(frame_output, dict):
                frame_pred = frame_output["clipwise_output"]
            else:
                frame_pred = frame_output

            all_frame_predictions.append(frame_pred.cpu().numpy())

            window_id += 1

        # 转换为 numpy array
        all_frame_predictions = np.array(all_frame_predictions)  # (num_windows, num_classes)

        # 1st Place 创新: 相邻窗口帧预测平均
        # 这是一种 1D 滑动窗口分割,类似于大图像的 2D 滑动窗口分割
        smoothed_predictions = self._smooth_predictions(all_frame_predictions)

        # 生成最终预测
        predictions = {}
        for window_id in range(len(smoothed_predictions)):
            row_id = f"soundscape_{window_id}_{self.window_size}"
            predictions[row_id] = {
                f"species_{i}": float(prob)
                for i, prob in enumerate(smoothed_predictions[window_id])
            }

        return predictions

    def _smooth_predictions(
        self,
        predictions: np.ndarray,
    ) -> np.ndarray:
        """
        平滑预测 - 使用高斯滤波和时间平均

        Args:
            predictions: (num_windows, num_classes)

        Returns:
            Smoothed predictions
        """
        # 1. 时间维度高斯平滑
        if self.smoothing_sigma > 0:
            smoothed = gaussian_filter1d(
                predictions,
                sigma=self.smoothing_sigma,
                axis=0,
                mode="nearest",
            )
        else:
            smoothed = predictions

        # 2. 相邻窗口平均1st Place 创新)
        # 使用相邻 3 个窗口的平均
        kernel_size = 3
        if len(smoothed) >= kernel_size:
            # Padding
            padded = np.pad(
                smoothed,
                ((kernel_size // 2, kernel_size // 2), (0, 0)),
                mode="edge",
            )

            # 一维卷积平均
            kernel = np.ones(kernel_size) / kernel_size
            averaged = np.zeros_like(smoothed)

            for c in range(smoothed.shape[1]):
                averaged[:, c] = np.convolve(
                    padded[:, c],
                    kernel,
                    mode="valid",
                )

            return averaged
        else:
            return smoothed

    def extract_mel(self, waveform: torch.Tensor) -> torch.Tensor:
        """提取 mel-spectrogram"""
        # 实际使用中应该与训练时的提取器一致
        pass


# 使用示例
inference = SlidingWindowInference(
    model=model,
    window_size=5,
    hop_size=5,
    smoothing_sigma=1.0,
)

predictions = inference.predict("test_soundscape_01.wav")

# 后处理(可选)
# - Delta shift: 调整低置信度类别的概率
# - Min-max 缩放
# - 频率范围调整

SED 模型架构BirdCLEF+ 2025 标准)

前排方案广泛使用的 SED (Sound Event Detection) 模型架构:

import torch
import torch.nn as nn
import timm

class SEDModel(nn.Module):
    """
    Sound Event Detection 模型

    参考 BirdCLEF 2023 2nd Place 和 BirdCLEF+ 2025 前排方案
    """

    def __init__(
        self,
        backbone: str = "tf_efficientnetv2_s.in21k",
        num_classes: int = 206,
        in_channels: int = 1,
        pretrained: bool = True,
    ):
        super().__init__()

        self.num_classes = num_classes

        # 使用 timm 的 EfficientNet 作为 backbone
        self.backbone = timm.create_model(
            backbone,
            pretrained=pretrained,
            in_chans=in_channels,
            num_classes=0,  # 移除分类头
        )

        # 获取 backbone 输出特征维度
        self.features_dim = self.backbone.num_features

        # 自定义注意力块 (6th Place AttBlockV2)
        self.att_block = AttBlockV2(
            self.features_dim,
            num_classes,
            activation="sigmoid",
        )

    def forward(self, x, return_segmentwise=False):
        """
        Args:
            x: (batch, in_channels, n_mels, time)
            return_segmentwise: 是否返回 segmentwise_logit

        Returns:
            如果 return_segmentwise=False:
                clipwise_output: (batch, num_classes)
            如果 return_segmentwise=True:
                dict with:
                    clipwise_output: (batch, num_classes)
                    segmentwise_output: (batch, num_classes, time_frames)
        """
        # Backbone 特征提取
        features = self.backbone(x)  # (batch, features_dim, time_frames)

        # 全局池化
        pooled_features = features.mean(dim=[2])  # (batch, features_dim)

        # 片级预测
        clipwise_output = self.att_block(pooled_features)  # (batch, num_classes)

        if not return_segmentwise:
            return clipwise_output

        # 帧级预测(用于伪标签生成)
        segmentwise_output = self.att_block(features)  # (batch, num_classes, time_frames)

        return {
            "clipwise_output": clipwise_output,
            "segmentwise_output": segmentwise_output,
        }


class AttBlockV2(nn.Module):
    """
    自定义注意力块 - 6th Place 创新

    使用 softmax 和 tanh 进行归一化,结合非线性变换
    """

    def __init__(
        self,
        in_features: int,
        out_features: int,
        activation: str = "sigmoid",
    ):
        super().__init__()

        self.activation = activation
        self.att = nn.Conv1d(in_features, out_features, kernel_size=1)
        self.cla = nn.Conv1d(in_features, out_features, kernel_size=1)

        # 初始化权重6th Place 关键)
        self.apply(self._init_weights)

    def _init_weights(self, m):
        if isinstance(m, nn.Conv1d):
            nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
            if m.bias is not None:
                nn.init.constant_(m.bias, 0)

    def forward(self, x):
        """
        Args:
            x: (batch, in_features, time_frames) 或 (batch, in_features)

        Returns:
            output: (batch, out_features) 或 (batch, out_features, time_frames)
        """
        if x.dim() == 2:
            # 全局池化特征
            x = x.unsqueeze(-1)  # (batch, in_features, 1)

        # 注意力权重
        att = self.att(x)
        att = torch.softmax(att, dim=1)  # 时间维度归一化

        # 分类特征
        cla = self.cla(x)

        # 加权求和
        output = torch.clamp(torch.clamp((cla * att).sum(dim=-1), min=1e-7, max=1-1e-7), min=1e-7)

        # 激活函数
        if self.activation == "sigmoid":
            output = torch.sigmoid(output)
        elif self.activation == "none":
            pass
        else:
            raise ValueError(f"Unknown activation: {self.activation}")

        return output.squeeze(-1) if output.size(-1) == 1 else output


# 常用 backbone 配置(前排方案)
BACKBONES = {
    "tf_efficientnetv2_s.in21k": {
        "features_dim": 1280,
        "description": "2nd Place 使用,平衡性能和速度",
    },
    "tf_efficientnetv2_b3.in21k": {
        "features_dim": 1536,
        "description": "6th Place 使用,更强性能",
    },
    "tf_efficientnetv2_m.in21k": {
        "features_dim": 2048,
        "description": "14th Place 使用,更高精度",
    },
    "eca_nfnet_l0": {
        "features_dim": 2304,
        "description": "2nd Place 使用,增加多样性",
    },
}

# 使用示例
model = SEDModel(
    backbone="tf_efficientnetv2_s.in21k",
    num_classes=206,
    in_channels=1,
    pretrained=True,
)

# 训练时:片级预测
clipwise_output = model(mel_spec)
loss = criterion(clipwise_output, labels)

# 伪标签生成时:帧级预测
output = model(mel_spec, return_segmentwise=True)
segmentwise_logits = output["segmentwise_output"]  # (batch, 206, time_frames)
segmentwise_probs = torch.sigmoid(segmentwise_logits)
# 时间维度平均得到更稳定的伪标签
avg_segmentwise_probs = segmentwise_probs.mean(dim=-1)  # (batch, 206)

前排方案详细技术分析

2nd Place - Xeno-Canto 预训练详细流程

作者: Volodymyr Vialactea 核心创新: 使用外部数据预训练 + 5秒音频片段训练

完整流程:

import torch
import torchaudio
import pandas as pd
from pathlib import Path

class XenoCantoPretraining:
    """
    2nd Place 方案Xeno-Canto 预训练流程

    关键点:
    1. 下载额外的 Xeno-Canto 数据
    2. 数据清洗和预处理
    3. 预训练
    4. 在主数据集上微调
    """

    def __init__(
        self,
        species_list: list,
        target_sample_rate: int = 32000,
        segment_duration: int = 5,
    ):
        self.species_list = species_list
        self.target_sample_rate = target_sample_rate
        self.segment_duration = segment_duration

    def download_xeno_canto_data(self, output_dir: str = "data/xeno_canto"):
        """
        步骤 1: 从 Xeno-Canto 下载数据

        注意事项:
        - 过滤掉当年比赛中的物种(避免数据泄漏)
        - 只下载高质量录音(评分 ≥ 3.0
        - 限制每个物种的下载量(避免数据不平衡)
        """
        # 使用 xeno-canto-api 或手动下载
        # 这里提供框架代码

        xc_species = [s for s in self.species_list if self._should_download(s)]

        for species in xc_species:
            # 调用 Xeno-Canto API
            # 下载音频文件
            # 保存到 output_dir/species_name/
            pass

    def _should_download(self, species: str) -> bool:
        """检查物种是否应该下载(避免数据泄漏)"""
        # 过滤比赛数据集中的物种
        competition_species = set(self._get_competition_species())
        return species not in competition_species

    def preprocess_xeno_canto(self, audio_dir: str):
        """
        步骤 2: 数据清洗和预处理

        2nd Place 的关键步骤:
        1. 去除人声(如果可能)
        2. 统一采样率到 32kHz
        3. 音频归一化
        4. 质量检查SNR、时长等
        """
        audio_files = list(Path(audio_dir).rglob("*.mp3"))

        cleaned_data = []

        for audio_file in audio_files:
            # 加载音频
            waveform, sr = torchaudio.load(audio_file)

            # 重采样到 32kHz
            if sr != self.target_sample_rate:
                resampler = torchaudio.transforms.Resample(sr, self.target_sample_rate)
                waveform = resampler(waveform)

            # 质量检查
            if self._check_quality(waveform):
                # 提取 5 秒片段
                segments = self._extract_segments(waveform)

                for segment in segments:
                    cleaned_data.append({
                        "file_path": str(audio_file),
                        "species": audio_file.parent.name,
                        "waveform": segment,
                    })

        return cleaned_data

    def _check_quality(self, waveform: torch.Tensor) -> bool:
        """质量检查"""
        # 检查 1: 时长至少 5 秒
        if waveform.shape[1] < self.target_sample_rate * self.segment_duration:
            return False

        # 检查 2: SNR信噪比
        # snr = self._calculate_snr(waveform)
        # if snr < 10:  # 最低 10dB
        #     return False

        # 检查 3: 削波检测
        if torch.abs(waveform).max() > 0.99:
            return False

        return True

    def _extract_segments(self, waveform: torch.Tensor) -> list:
        """
        提取 5 秒音频片段

        2nd Place 使用了多种采样策略:
        1. 随机采样
        2. 基于能量的采样RMS
        3. 重叠采样
        """
        segment_samples = self.segment_duration * self.target_sample_rate

        if waveform.shape[1] <= segment_samples:
            # 填充到 5 秒
            padding = segment_samples - waveform.shape[1]
            waveform = torch.nn.functional.pad(waveform, (0, padding))
            return [waveform]

        # 方法 1: 随机采样
        # 2nd Place 尝试了多种方法,最终发现随机采样效果最好

        # 方法 2: 基于能量的采样RMS
        # 计算每个 5 秒窗口的 RMS 能量
        # 选择能量最高的窗口

        # 方法 3: 重叠采样
        # 滑动窗口hop_size = 2.5 秒

        # 这里实现随机采样
        max_start = waveform.shape[1] - segment_samples
        start_idx = torch.randint(0, max_start, (1,)).item()

        segment = waveform[:, start_idx:start_idx + segment_samples]
        return [segment]

    def pretrain(self, xc_data, model, save_path: str = "checkpoints/pretrained.pth"):
        """
        步骤 3: 预训练

        2nd Place 的预训练策略:
        - 使用 Xeno-Canto 数据训练
        - BCE Loss
        - SpecAugment 增强
        - 50-100 epochs
        """
        # 创建 dataloader
        train_loader = self._create_dataloader(xc_data)

        # 优化器
        optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)

        # 学习率调度器
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=50, eta_min=1e-6
        )

        # 损失函数
        criterion = nn.BCEWithLogitsLoss()

        # 训练循环
        model.train()
        for epoch in range(50):  # 50 epochs
            for batch in train_loader:
                mel_spec = self._extract_mel(batch["waveform"])
                labels = batch["labels"]

                # 前向传播
                logits = model(mel_spec)
                loss = criterion(logits, labels)

                # 反向传播
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

            scheduler.step()

            print(f"Epoch {epoch+1}/50, Loss: {loss.item():.4f}")

        # 保存预训练模型
        torch.save(model.state_dict(), save_path)
        print(f"Pretrained model saved to {save_path}")

    def finetune(self, model, train_data, val_data, pretrained_path: str):
        """
        步骤 4: 微调

        2nd Place 的微调策略:
        - 加载预训练权重
        - 使用更小的学习率
        - 选择最佳 checkpoint不是最后一个
        - 关键AUC 从 0.83-0.84 跳升至 0.86-0.87
        """
        # 加载预训练权重
        model.load_state_dict(torch.load(pretrained_path))

        # 优化器(更小的学习率)
        optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

        # 学习率调度器
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=30, eta_min=1e-7
        )

        # 损失函数
        criterion = nn.BCEWithLogitsLoss()

        best_val_score = 0
        best_epoch = 0

        # 微调循环
        model.train()
        for epoch in range(30):  # 30 epochs
            # 训练
            for batch in train_data:
                mel_spec = self._extract_mel(batch["waveform"])
                labels = batch["labels"]

                logits = model(mel_spec)
                loss = criterion(logits, labels)

                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

            # 验证
            val_score = self._validate(model, val_data)

            print(f"Epoch {epoch+1}/30, Val AUC: {val_score:.4f}")

            # 保存最佳模型
            if val_score > best_val_score:
                best_val_score = val_score
                best_epoch = epoch
                torch.save(model.state_dict(), f"checkpoints/best_finetuned_epoch{epoch}.pth")

            scheduler.step()

        print(f"Best epoch: {best_epoch}, Best Val AUC: {best_val_score:.4f}")

    def _extract_mel(self, waveform: torch.Tensor) -> torch.Tensor:
        """提取 mel-spectrogram应该与训练时一致"""
        # 实现 mel-spectrogram 提取
        pass

    def _create_dataloader(self, data):
        """创建 dataloader"""
        pass

    def _validate(self, model, val_data):
        """验证"""
        pass

    def _get_competition_species(self) -> list:
        """获取竞赛数据集中的物种(避免数据泄漏)"""
        pass

    def _calculate_snr(self, waveform: torch.Tensor) -> float:
        """计算 SNR"""
        pass


# 2nd Place 关键技术总结
"""
关键发现(来自 2nd Place writeup

1. **预训练效果显著**
   - 无预训练AUC 0.83-0.84
   - 有预训练AUC 0.86-0.87
   - 提升:+0.02-0.03 AUC

2. **Checkpoint 选择很重要**
   - 不是最后一个 epoch 最好
   - 需要验证集选择最佳 checkpoint
   - 通常在 epoch 10-20 之间

3. **采样策略**
   - 随机采样效果最好
   - 基于能量的采样没有明显优势
   - 5 秒片段是最佳长度

4. **数据增强**
   - SpecAugment 必须保留
   - RandomFiltering 有效
   - 即使关闭略微提高 CV但保留确保 LB 稳定性
"""

5th Place - Self-Distillation 详细实现

作者: Noir 核心创新: 三阶段自蒸馏 + Silero VAD 数据清洗

完整流程:

import torch
import torch.nn as nn
import numpy as np

class SelfDistillationTrainer:
    """
    5th Place 方案Self-Distillation 三阶段训练

    核心思想:
    1. 使用 Silero VAD 去除人声
    2. 三阶段自蒸馏训练
    3. 迭代丰富次要标签
    """

    def __init__(
        self,
        model: nn.Module,
        num_classes: int = 206,
    ):
        self.model = model
        self.num_classes = num_classes

    def stage1_initial_training(self, train_loader, val_loader, epochs=30):
        """
        阶段 1: 初始训练

        使用清洗后的训练音频train_audio进行初始训练
        """
        print("=== Stage 1: Initial Training ===")

        optimizer = torch.optim.AdamW(self.model.parameters(), lr=1e-3)
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=epochs, eta_min=1e-6
        )
        criterion = nn.BCEWithLogitsLoss()

        best_val_loss = float('inf')

        for epoch in range(epochs):
            self.model.train()
            train_loss = 0

            for batch in train_loader:
                mel_spec = batch['mel_spec']
                labels = batch['labels']

                # 前向传播
                logits = self.model(mel_spec)
                loss = criterion(logits, labels)

                # 反向传播
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

                train_loss += loss.item()

            # 验证
            val_loss = self._validate(self.model, val_loader, criterion)

            # 学习率更新
            scheduler.step()

            print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss/len(train_loader):.4f}, "
                  f"Val Loss: {val_loss:.4f}")

            # 保存最佳模型
            if val_loss < best_val_loss:
                best_val_loss = val_loss
                torch.save(self.model.state_dict(), "checkpoints/stage1_best.pth")

        print(f"Stage 1 complete. Best Val Loss: {best_val_loss:.4f}")

        # 加载最佳模型用于下一阶段
        self.model.load_state_dict(torch.load("checkpoints/stage1_best.pth"))

    def stage2_self_distillation_train_audio(
        self,
        train_loader,
        epochs=20,
        temperature=3.0,
        alpha=0.7,
    ):
        """
        阶段 2: 使用 train_audio 的自蒸馏

        使用 stage 1 模型的预测作为软标签进行蒸馏
        """
        print("=== Stage 2: Self-Distillation on train_audio ===")

        # stage 1 模型作为教师
        teacher_model = type(self.model)(
            backbone=self.model.backbone,
            num_classes=self.num_classes,
        )
        teacher_model.load_state_dict(torch.load("checkpoints/stage1_best.pth"))
        teacher_model.eval()

        # 学生模型(可以重置权重或继续训练)
        # 5th Place 选择继续训练

        optimizer = torch.optim.AdamW(self.model.parameters(), lr=5e-4)  # 更小的学习率
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=epochs, eta_min=1e-7
        )

        # 蒸馏损失
        distillation_criterion = nn.KLDivLoss(reduction="batchmean")
        bce_criterion = nn.BCEWithLogitsLoss()

        best_val_loss = float('inf')

        for epoch in range(epochs):
            self.model.train()
            train_loss = 0

            for batch in train_loader:
                mel_spec = batch['mel_spec']
                hard_labels = batch['labels']

                with torch.no_grad():
                    # 教师模型预测(软标签)
                    teacher_logits = teacher_model(mel_spec)
                    teacher_probs = torch.sigmoid(teacher_logits / temperature)

                # 学生模型预测
                student_logits = self.model(mel_spec)
                student_log_probs = torch.log_softmax(student_logits / temperature, dim=-1)

                # 蒸馏损失
                distill_loss = distillation_criterion(student_log_probs, teacher_probs)

                # 硬标签损失
                bce_loss = bce_criterion(student_logits, hard_labels)

                # 组合损失
                loss = alpha * (temperature ** 2) * distill_loss + (1 - alpha) * bce_loss

                # 反向传播
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

                train_loss += loss.item()

            # 验证
            val_loss = self._validate(self.model, train_loader, bce_criterion)  # 用训练集验证

            scheduler.step()

            print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss/len(train_loader):.4f}, "
                  f"Val Loss: {val_loss:.4f}")

            if val_loss < best_val_loss:
                best_val_loss = val_loss
                torch.save(self.model.state_dict(), "checkpoints/stage2_best.pth")

        print(f"Stage 2 complete. Best Val Loss: {best_val_loss:.4f}")

        self.model.load_state_dict(torch.load("checkpoints/stage2_best.pth"))

    def stage3_self_distillation_soundscape(
        self,
        train_audio_loader,
        soundscape_files,
        epochs=20,
        temperature=3.0,
        alpha=0.5,  # 更重视伪标签
    ):
        """
        阶段 3: 结合 train_audio 和 train_soundscapes 的自蒸馏

        关键创新:丰富次要标签
        - 许多音频包含未标注的鸟叫声
        - 通过自蒸馏发现这些次要标签
        """
        print("=== Stage 3: Self-Distillation with soundscape ===")

        # stage 2 模型作为教师
        teacher_model = type(self.model)(
            backbone=self.model.backbone,
            num_classes=self.num_classes,
        )
        teacher_model.load_state_dict(torch.load("checkpoints/stage2_best.pth"))
        teacher_model.eval()

        optimizer = torch.optim.AdamW(self.model.parameters(), lr=3e-4)
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=epochs, eta_min=1e-7
        )

        distillation_criterion = nn.KLDivLoss(reduction="batchmean")
        bce_criterion = nn.BCEWithLogitsLoss()

        # 生成 soundscape 的伪标签
        soundscape_pseudo_labels = self._generate_pseudo_labels(
            teacher_model, soundscape_files
        )

        # 合并 train_audio 和 soundscape 数据
        # 50% train_audio + 50% soundscape

        best_val_loss = float('inf')

        for epoch in range(epochs):
            self.model.train()
            train_loss = 0

            # 训练 train_audio带硬标签
            for batch in train_audio_loader:
                if np.random.rand() > 0.5:
                    continue  # 50% 概率使用 train_audio

                mel_spec = batch['mel_spec']
                hard_labels = batch['labels']

                with torch.no_grad():
                    teacher_logits = teacher_model(mel_spec)
                    teacher_probs = torch.sigmoid(teacher_logits / temperature)

                student_logits = self.model(mel_spec)
                student_log_probs = torch.log_softmax(student_logits / temperature, dim=-1)

                distill_loss = distillation_criterion(student_log_probs, teacher_probs)
                bce_loss = bce_criterion(student_logits, hard_labels)
                loss = alpha * (temperature ** 2) * distill_loss + (1 - alpha) * bce_loss

                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

                train_loss += loss.item()

            # 训练 soundscape伪标签
            for batch in soundscape_pseudo_labels:
                if np.random.rand() <= 0.5:
                    continue  # 50% 概率使用 soundscape

                mel_spec = batch['mel_spec']
                pseudo_labels = batch['labels']  # 软标签

                with torch.no_grad():
                    teacher_logits = teacher_model(mel_spec)
                    teacher_probs = torch.sigmoid(teacher_logits / temperature)

                student_logits = self.model(mel_spec)
                student_log_probs = torch.log_softmax(student_logits / temperature, dim=-1)

                # 只使用蒸馏损失(没有硬标签)
                distill_loss = distillation_criterion(student_log_probs, teacher_probs)

                optimizer.zero_grad()
                distill_loss.backward()
                optimizer.step()

                train_loss += distill_loss.item()

            scheduler.step()

            print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}")

            # 保存检查点
            if epoch % 5 == 0:
                torch.save(self.model.state_dict(), f"checkpoints/stage3_epoch{epoch}.pth")

        print("Stage 3 complete")

    def _generate_pseudo_labels(
        self,
        model: nn.Module,
        audio_files: list,
    ) -> list:
        """
        生成 soundscape 的伪标签

        关键:丰富次要标签
        - 使用帧级预测segmentwise
        - 时间维度平均
        """
        model.eval()
        pseudo_labels = []

        with torch.no_grad():
            for audio_file in audio_files:
                # 加载音频
                waveform, sr = torchaudio.load(audio_file)

                # 分段处理5秒窗口
                segments = self._split_audio(waveform, sr)

                for segment in segments:
                    mel_spec = self._extract_mel(segment)

                    # 获取帧级预测
                    output = model(mel_spec, return_segmentwise=True)
                    segmentwise_logits = output["segmentwise_output"]  # (1, 206, time)
                    segmentwise_probs = torch.sigmoid(segmentwise_logits)

                    # 时间维度平均(关键:丰富次要标签)
                    avg_probs = segmentwise_probs.mean(dim=-1).squeeze(0)  # (206,)

                    pseudo_labels.append({
                        "mel_spec": mel_spec,
                        "labels": avg_probs,
                    })

        return pseudo_labels

    def _split_audio(self, waveform: torch.Tensor, sr: int) -> list:
        """分段处理音频"""
        segment_samples = 5 * sr
        segments = []

        for i in range(0, waveform.shape[1], segment_samples):
            segment = waveform[:, i:i+segment_samples]
            if segment.shape[1] == segment_samples:
                segments.append(segment)
            else:
                # 填充
                padding = segment_samples - segment.shape[1]
                segment = torch.nn.functional.pad(segment, (0, padding))
                segments.append(segment)

        return segments

    def _extract_mel(self, waveform: torch.Tensor) -> torch.Tensor:
        """提取 mel-spectrogram"""
        pass

    def _validate(self, model, val_loader, criterion):
        """验证"""
        model.eval()
        total_loss = 0

        with torch.no_grad():
            for batch in val_loader:
                mel_spec = batch['mel_spec']
                labels = batch['labels']

                logits = model(mel_spec)
                loss = criterion(logits, labels)
                total_loss += loss.item()

        return total_loss / len(val_loader)


class SileroVADDataCleaner:
    """
    Silero VAD 数据清洗

    5th Place 使用 Silero VAD 检测并去除人声片段
    """

    def __init__(self):
        # 加载 Silero VAD 模型
        self.model, utils = torch.hub.load(
            repo_or_dir='snakers4/silero-vad',
            model='silero_vad',
            force_reload=False,
            onnx=False
        )
        self.model.eval()

    def clean_audio(self, audio_path: str, output_path: str):
        """
        去除包含人声的音频片段

        Returns:
            清洗后的音频(去除人声部分)
        """
        waveform, sr = torchaudio.load(audio_path)

        # 转换为单声道
        if waveform.shape[0] > 1:
            waveform = waveform.mean(dim=0, keepdim=True)

        # 重采样到 16kHzSilero VAD 要求)
        if sr != 16000:
            resampler = torchaudio.transforms.Resample(sr, 16000)
            waveform = resampler(waveform)
            sr = 16000

        # VAD 检测
        speech_chunks = self._detect_speech(waveform, sr)

        # 如果检测到人声,去除这些片段
        if speech_chunks:
            cleaned_waveform = self._remove_speech_chunks(waveform, speech_chunks)
        else:
            cleaned_waveform = waveform

        # 保存清洗后的音频
        torchaudio.save(output_path, cleaned_waveform, sr)

        return cleaned_waveform

    def _detect_speech(self, waveform: torch.Tensor, sr: int) -> list:
        """
        检测人声片段

        Returns:
            List of (start_ms, end_ms) tuples
        """
        # 获取语音概率
        speech_probs = []
        window_size = 512  # 32ms at 16kHz

        for i in range(0, waveform.shape[1], window_size):
            chunk = waveform[:, i:i+window_size]
            if chunk.shape[1] < window_size:
                continue

            with torch.no_grad():
                speech_prob = self.model(chunk, sr).item()
                speech_probs.append(speech_prob)

        # 阈值检测(人声概率 > 0.5
        speech_chunks = []
        in_speech = False
        start_idx = 0

        for i, prob in enumerate(speech_probs):
            if prob > 0.5 and not in_speech:
                in_speech = True
                start_idx = i * window_size
            elif prob <= 0.5 and in_speech:
                in_speech = False
                end_idx = i * window_size
                speech_chunks.append((start_idx, end_idx))

        # 转换为毫秒
        speech_chunks_ms = [(s * 1000 / sr, e * 1000 / sr) for s, e in speech_chunks]

        return speech_chunks_ms

    def _remove_speech_chunks(
        self,
        waveform: torch.Tensor,
        speech_chunks: list,
    ) -> torch.Tensor:
        """去除人声片段"""
        sr = 16000

        # 将时间转换为样本索引
        speech_ranges = [(int(s * sr / 1000), int(e * sr / 1000)) for s, e in speech_chunks]

        # 创建掩码True 表示保留)
        mask = torch.ones(waveform.shape[1], dtype=torch.bool)

        for start, end in speech_ranges:
            mask[start:end] = False

        # 应用掩码
        cleaned_waveform = waveform[:, mask]

        return cleaned_waveform


# 5th Place 关键技术总结
"""
关键发现(来自 5th Place writeup

1. **Silero VAD 有效**
   - 去除人声减少误检
   - 清洗后数据质量提升

2. **三阶段自蒸馏**
   - Stage 1: 基础训练
   - Stage 2: train_audio 自蒸馏
   - Stage 3: 加入 soundscape 伪标签
   - 每个阶段都带来提升

3. **丰富次要标签**
   - 许多音频包含未标注的鸟叫声
   - 使用帧级预测和时间平均
   - 迭代训练发现更多标签

4. **数据平衡重要**
   - 样本 <20 的类别复制到 20
   - 样本 <30 的类别手动筛选
   - 使用前 30/60 秒数据
"""

1st Place - Multi-Iterative Noisy Student 详细流程

作者: Nikita Babych 核心创新: 多迭代 Noisy Student + MixUp + 幂次变换

完整流程:

import torch
import torch.nn as nn
import numpy as np

class MultiIterativeNoisyStudent:
    """
    1st Place 方案:多迭代 Noisy Student 自训练

    核心创新:
    1. 多迭代自训练,每次使用 MixUp
    2. 伪标签幂次变换减少噪声
    3. 滑动窗口推理,帧预测平均
    """

    def __init__(
        self,
        model: nn.Module,
        num_classes: int = 206,
        num_iterations: int = 3,
    ):
        self.model = model
        self.num_classes = num_classes
        self.num_iterations = num_iterations

        # 1st Place 关键参数
        self.mixup_alpha = 0.5
        self.power_transform = 1.5  # 幂次变换参数(减少伪标签噪声)

    def train_iteration(
        self,
        train_audio_loader,
        train_soundscape_files,
        iteration: int,
        epochs=30,
    ):
        """
        执行一次 Noisy Student 迭代

        Args:
            iteration: 当前迭代编号0, 1, 2, ...
        """
        print(f"=== Noisy Student Iteration {iteration + 1} ===")

        # 准备数据
        # 50% train_audio + 50% 伪标签 soundscape
        if iteration == 0:
            # 第一次迭代:只使用 train_audio
            train_loader = train_audio_loader
        else:
            # 后续迭代:混合 train_audio 和伪标签
            train_loader = self._prepare_mixed_data(
                train_audio_loader,
                train_soundscape_files,
                iteration,
            )

        # 优化器
        optimizer = torch.optim.AdamW(self.model.parameters(), lr=1e-3)
        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
            optimizer, T_max=epochs, eta_min=1e-6
        )
        criterion = nn.BCEWithLogitsLoss()

        best_val_loss = float('inf')

        for epoch in range(epochs):
            self.model.train()
            train_loss = 0

            for batch in train_loader:
                mel_spec = batch['mel_spec']
                labels = batch['labels']

                # MixUp 数据增强1st Place 关键)
                if np.random.rand() < 0.5:  # 50% 概率应用 MixUp
                    mel_spec, labels = self._apply_mixup(mel_spec, labels)

                # 前向传播
                logits = self.model(mel_spec)
                loss = criterion(logits, labels)

                # 反向传播
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

                train_loss += loss.item()

            # 验证(使用训练集的一个子集)
            val_loss = self._quick_validate(train_audio_loader, criterion)

            scheduler.step()

            print(f"Iteration {iteration+1}, Epoch {epoch+1}/{epochs}, "
                  f"Train Loss: {train_loss/len(train_loader):.4f}, "
                  f"Val Loss: {val_loss:.4f}")

            if val_loss < best_val_loss:
                best_val_loss = val_loss
                torch.save(self.model.state_dict(),
                          f"checkpoints/noisy_student_iter{iteration}_best.pth")

        print(f"Iteration {iteration+1} complete. Best Val Loss: {best_val_loss:.4f}")

    def _prepare_mixed_data(
        self,
        train_audio_loader,
        soundscape_files,
        iteration: int,
    ):
        """
        准备混合数据train_audio + 伪标签 soundscape

        关键幂次变换减少伪标签噪声1st Place 创新)
        """
        # 生成伪标签
        pseudo_labels = self._generate_pseudo_labels_power_transform(
            soundscape_files,
            self.power_transform,
        )

        # 创建混合 dataloader
        mixed_data = []

        # 添加 train_audio
        for batch in train_audio_loader:
            mixed_data.append(batch)

        # 添加伪标签 soundscape
        for item in pseudo_labels:
            mixed_data.append(item)

        # 打乱顺序
        np.random.shuffle(mixed_data)

        return mixed_data

    def _generate_pseudo_labels_power_transform(
        self,
        audio_files: list,
        power: float = 1.5,
    ) -> list:
        """
        生成伪标签并应用幂次变换

        1st Place 关键创新:幂次变换减少噪声

        原理:
        - 直接对概率进行温度缩放会提高噪声的概率
        - 通过幂次变换,防止噪声的放大,并保留重要的标签信号
        """
        self.model.eval()
        pseudo_labels = []

        with torch.no_grad():
            for audio_file in audio_files:
                waveform, sr = torchaudio.load(audio_file)

                # 分段处理5秒窗口
                segments = self._split_audio(waveform, sr)

                for segment in segments:
                    mel_spec = self._extract_mel(segment)

                    # 获取预测
                    logits = self.model(mel_spec)
                    probs = torch.sigmoid(logits).squeeze(0).cpu().numpy()  # (206,)

                    # 幂次变换1st Place 创新)
                    # power > 1: 压缩低概率,扩展高概率
                    # power < 1: 扩展低概率,压缩高概率
                    probs_transformed = np.power(probs, power)

                    pseudo_labels.append({
                        "mel_spec": mel_spec,
                        "labels": torch.tensor(probs_transformed, dtype=torch.float32),
                    })

        return pseudo_labels

    def _apply_mixup(
        self,
        mel_spec: torch.Tensor,
        labels: torch.Tensor,
    ) -> tuple:
        """
        MixUp 数据增强

        1st Place 关键:使用固定混合权重 0.5
        """
        batch_size = mel_spec.size(0)

        # 生成混合权重
        lam = np.random.beta(self.mixup_alpha, self.mixup_alpha)
        # 1st Place 发现固定权重 0.5 效果更好
        # lam = 0.5

        # 随机排列
        index = torch.randperm(batch_size)

        # 混合特征
        mixed_mel = lam * mel_spec + (1 - lam) * mel_spec[index]

        # 混合标签(取最大值)
        mixed_labels = torch.maximum(labels, labels[index])

        return mixed_mel, mixed_labels

    def _split_audio(self, waveform: torch.Tensor, sr: int) -> list:
        """分段处理音频"""
        segment_samples = 5 * sr
        segments = []

        for i in range(0, waveform.shape[1], segment_samples):
            segment = waveform[:, i:i+segment_samples]
            if segment.shape[1] == segment_samples:
                segments.append(segment)
            else:
                padding = segment_samples - segment.shape[1]
                segment = torch.nn.functional.pad(segment, (0, padding))
                segments.append(segment)

        return segments

    def _extract_mel(self, waveform: torch.Tensor) -> torch.Tensor:
        """提取 mel-spectrogram"""
        pass

    def _quick_validate(self, val_loader, criterion):
        """快速验证"""
        self.model.eval()
        total_loss = 0
        count = 0

        with torch.no_grad():
            for i, batch in enumerate(val_loader):
                if i >= 10:  # 只验证前 10 个 batch
                    break

                mel_spec = batch['mel_spec']
                labels = batch['labels']

                logits = self.model(mel_spec)
                loss = criterion(logits, labels)
                total_loss += loss.item()
                count += 1

        return total_loss / max(count, 1)


# 1st Place 关键技术总结
"""
关键发现(来自 1st Place writeup

1. **多迭代 Noisy Student 有效**
   - 每次迭代都带来提升
   - 3 次迭代是最优的
   - 更多迭代可能导致噪声累积

2. **幂次变换是关键**
   - 直接使用伪标签:性能提升有限
   - 幂次变换power=1.5):显著提升
   - 防止噪声放大,保留信号

3. **MixUp 策略**
   - 固定权重 0.5 比随机权重更稳定
   - 迫使模型学习更鲁棒的特征
   - 减少过拟合

4. **滑动窗口推理**
   - 使用帧预测的平均值
   - 避免丢弃有价值的预测数据
   - 类似图像的 2D 滑动窗口分割
"""


# 1st Place 完整训练流程示例
def train_noisy_student_full_pipeline():
    """
    完整的 Noisy Student 训练流程
    """
    # 初始化
    model = SEDModel(num_classes=206)
    trainer = MultiIterativeNoisyStudent(model, num_iterations=3)

    # 准备数据
    train_audio_loader = ...  # 训练音频 loader
    soundscape_files = ...    # soundscape 文件列表

    # 迭代 0: 只使用 train_audio
    print("=== Iteration 0: Training on train_audio only ===")
    trainer.train_iteration(train_audio_loader, soundscape_files, iteration=0, epochs=30)

    # 迭代 1: 加入伪标签 soundscape
    print("=== Iteration 1: Adding pseudo-labeled soundscape ===")
    trainer.train_iteration(train_audio_loader, soundscape_files, iteration=1, epochs=30)

    # 迭代 2: 使用新的伪标签
    print("=== Iteration 2: Refreshing pseudo labels ===")
    trainer.train_iteration(train_audio_loader, soundscape_files, iteration=2, epochs=30)

    # 最终集成:使用不同迭代的模型
    model_iter0 = SEDModel(num_classes=206)
    model_iter0.load_state_dict(torch.load("checkpoints/noisy_student_iter0_best.pth"))

    model_iter1 = SEDModel(num_classes=206)
    model_iter1.load_state_dict(torch.load("checkpoints/noisy_student_iter1_best.pth"))

    model_iter2 = SEDModel(num_classes=206)
    model_iter2.load_state_dict(torch.load("checkpoints/noisy_student_iter2_best.pth"))

    # 集成预测
    def ensemble_predict(mel_spec):
        pred0 = torch.sigmoid(model_iter0(mel_spec))
        pred1 = torch.sigmoid(model_iter1(mel_spec))
        pred2 = torch.sigmoid(model_iter2(mel_spec))

        # 简单平均
        ensemble_pred = (pred0 + pred1 + pred2) / 3
        return ensemble_pred

    return ensemble_predict

4th Place - Soft AUC Loss 详细分析

作者: dylan.liu 核心创新: 支持软标签的 AUC 损失函数

问题背景:

  • 标准 AUC 损失函数不支持软标签(适用于知识蒸馏和半监督学习)
  • 4th Place 通过自定义 soft AUC loss 解决这个问题
  • 效果:从 11 名跃升至 4 名LB 从 0.850 → 0.901
import torch
import torch.nn as nn
import torch.nn.functional as F

class SoftAUCLoss_v4(nn.Module):
    """
    4th Place Soft AUC Loss 实现

    参考4th Place writeup
    效果LB 从 0.850 → 0.901(显著提升)

    核心思想:
    1. 支持 soft labels适用于知识蒸馏和半监督学习
    2. 通过正负样本对的排序关系优化 AUC
    3. 减少 overfitting
    """

    def __init__(
        self,
        margin: float = 1.0,
        reduction: str = "mean",
    ):
        super().__init__()
        self.margin = margin
        self.reduction = reduction

    def forward(
        self,
        predictions: torch.Tensor,
        targets: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            predictions: (batch, num_classes) - 原始 logits
            targets: (batch, num_classes) - 软标签 [0, 1]

        Returns:
            AUC loss
        """
        probs = torch.sigmoid(predictions)
        num_classes = predictions.size(1)
        losses = []

        for c in range(num_classes):
            prob_c = probs[:, c]      # (batch,)
            target_c = targets[:, c]  # (batch,)

            # 计算所有样本对的差异
            # 对于软标签,我们需要加权处理

            # 创建样本对矩阵
            # target_diff > 0 表示 target_i > target_j
            target_diff = target_c.unsqueeze(1) - target_c.unsqueeze(0)  # (batch, batch)
            prob_diff = prob_c.unsqueeze(1) - prob_c.unsqueeze(0)        # (batch, batch)

            # 只考虑 target_i > target_j 的对
            # 即正样本(高 target应该有更高的预测概率
            mask = target_diff > 0

            if mask.sum() > 0:
                # Hinge loss: max(0, margin - (prob_i - prob_j))
                # 理想情况prob_i > prob_j正样本预测高于负样本
                # margin - (prob_i - prob_j) 应该 <= 0
                # 如果 > 0说明违反了排序关系

                loss_c = F.relu(self.margin - prob_diff)[mask].mean()

                # 4th Place 发现加权版本更有效
                # 使用 target_diff 作为权重
                # weight = target_diff[mask]
                # weighted_loss = F.relu(self.margin - prob_diff)[mask] * weight
                # loss_c = weighted_loss.sum() / weight.sum()

                losses.append(loss_c)

        if len(losses) == 0:
            return torch.tensor(0.0, device=predictions.device, requires_grad=True)

        losses = torch.stack(losses)

        if self.reduction == "mean":
            return losses.mean()
        elif self.reduction == "sum":
            return losses.sum()
        else:
            return losses


class SoftAUCLoss_Advanced(nn.Module):
    """
    改进的 Soft AUC Loss

    结合 4th Place 的发现和其他优化:
    1. 温度缩放
    2. 自适应 margin
    3. 类别加权
    """

    def __init__(
        self,
        margin: float = 1.0,
        temperature: float = 1.0,
        use_class_weighting: bool = True,
    ):
        super().__init__()
        self.margin = margin
        self.temperature = temperature
        self.use_class_weighting = use_class_weighting

    def forward(
        self,
        predictions: torch.Tensor,
        targets: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            predictions: (batch, num_classes)
            targets: (batch, num_classes) - 软标签
        """
        # 温度缩放
        probs = torch.sigmoid(predictions / self.temperature)
        num_classes = predictions.size(1)

        losses = []

        for c in range(num_classes):
            prob_c = probs[:, c]
            target_c = targets[:, c]

            # 样本对矩阵
            target_diff = target_c.unsqueeze(1) - target_c.unsqueeze(0)
            prob_diff = prob_c.unsqueeze(1) - prob_c.unsqueeze(0)

            # mask: target_i > target_j
            mask = target_diff > 0

            if mask.sum() > 0:
                # Hinge loss
                base_loss = F.relu(self.margin - prob_diff)[mask]

                # 可选:使用 target_diff 作为权重
                # 这给予高 target 差异的样本对更高权重
                weights = target_diff[mask]
                weighted_loss = base_loss * weights

                loss_c = weighted_loss.sum() / weights.sum()

                # 可选:类别权重(处理长尾分布)
                if self.use_class_weighting:
                    # 稀有类别更高权重
                    class_weight = self._get_class_weight(c, num_classes)
                    loss_c = loss_c * class_weight

                losses.append(loss_c)

        if len(losses) == 0:
            return torch.tensor(0.0, device=predictions.device, requires_grad=True)

        return torch.stack(losses).mean()

    def _get_class_weight(self, class_idx: int, num_classes: int) -> float:
        """
        计算类别权重(处理长尾分布)

        简单版本:可以基于样本频率
        """
        # 这里使用简单策略:可以替换为实际的类别频率
        #稀有类获得更高权重
        return 1.0  # 可以自定义


# 4th Place 关键发现总结
"""
关键发现(来自 4th Place writeup

1. **Soft AUC Loss 显著提升性能**
   - LB 从 0.850 → 0.901
   - 排名从 11 名 → 4 名
   - +0.05 AUC 提升是巨大的

2. **为什么 Soft AUC Loss 有效**
   - 标准 AUC loss 只支持硬标签0 或 1
   - Soft AUC Loss 支持软标签0 到 1 之间)
   - 适用于知识蒸馏和半监督学习
   - 减少 overfitting

3. **实现细节**
   - 使用样本对的排序关系
   - Hinge loss: max(0, margin - (prob_i - prob_j))
   - 只考虑 target_i > target_j 的对
   - margin 通常设为 1.0

4. **适用场景**
   - 半监督学习(伪标签)
   - 知识蒸馏(软标签)
   - 长尾分布(稀有类别)
   - 标签噪声(软标签更鲁棒)

5. **与其他损失函数对比**
   - BCE Loss: 简单但易过拟合
   - Focal Loss: 处理类别不平衡,但不优化 AUC
   - Soft AUC Loss: 直接优化 AUC支持软标签
"""


# 使用示例
def train_with_soft_auc_loss():
    """使用 Soft AUC Loss 训练"""

    model = SEDModel(num_classes=206)

    # 标准训练BCE Loss
    criterion_bce = nn.BCEWithLogitsLoss()

    # 半监督训练Soft AUC Loss
    criterion_soft_auc = SoftAUCLoss_v4(margin=1.0)

    # 优化器
    optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3)

    # 训练循环
    for epoch in range(30):
        model.train()

        for batch in train_loader:
            mel_spec = batch['mel_spec']
            labels = batch['labels']  # 可能是软标签

            # 选择损失函数
            if batch.get('is_pseudo', False):  # 伪标签数据
                # 使用 Soft AUC Loss
                loss = criterion_soft_auc(model(mel_spec), labels)
            else:  # 真实标签
                # 可以使用 BCE Loss 或 Soft AUC Loss
                loss = criterion_bce(model(mel_spec), labels)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        print(f"Epoch {epoch+1}/30, Loss: {loss.item():.4f}")

Best Practices

时间序列分类竞赛策略

策略 何时使用 说明
CWT over STFT 非平稳信号 CWT提供更好的时间-频率局部化
Entmax over Softmax 标签稀疏时 Entmax产生更稀疏的输出
非负线性回归集成 多模型集成时 即使过拟合也能保持相关性
2-Stage Training 标签质量不均时 Stage1全数据Stage2高质量样本
Group K-Fold 有重复样本时 确保同一patient/EEG不分散
仅用高质量样本 评估时 使用votes≥10的样本建立验证集

时频分析方法对比

方法 优点 缺点 适用场景
STFT 简单,易实现 固定窗口,时频分辨率权衡 平稳信号
CWT 多分辨率分析,捕捉局部特征 需要选择小波函数 非平稳信号EEG
Superlet 最高时频分辨率 计算成本高 复杂脑波模式

频率配置经验

配置 范围 说明
标准CWT 0.5-20 Hz Kaggle提供的spectrogram默认范围
扩展CWT 0.5-40 Hz 更好的结果 (suguuuuu)
带通滤波 0.5-40 Hz 高频噪声增加'other'投票

数据增强策略

时间序列 (1D):

  • 随机时间偏移 (±5秒)
  • 随机带通滤波 (不同频率范围)
  • 通道翻转 (水平/垂直)
  • 幅值缩放

Scalogram/Spectrogram (2D):

  • XYMasking (随机遮挡)
  • Mixup
  • 时间方向翻转

Backbone选择

时间序列 (1D):

  • 1D CNN + GRU
  • Transformer (Time-series Transformer)
  • LSTM/GRU

Scalogram (2D):

  • SwinV2: swinv2_tiny_window16 (最佳: CV 0.2229)
  • MaxVIT: maxvit_base_tf_512
  • ConvNeXt: convnextv2_atto

标签处理技巧

技巧 效果
标签平滑 (加0.02 offset) 使低投票数标签获得更强正则化
仅用votes≥10评估 CV/LB相关性接近1:1
投票数归一化 多专家投票转换为分布

常见误区

误区 正确做法
STFT不够好就放弃时频分析 尝试CWT或Superlet
Softmax输出不够稀疏 使用Entmax
集成权重手动调参 使用非负线性回归
用全部样本验证 仅用高质量样本 (votes≥10)
忽略Group K-Fold 防止同一patient的数据泄露