{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21669,"databundleVersionId":1692278,"sourceType":"competition"},{"sourceId":14168168,"sourceType":"datasetVersion","datasetId":9031106}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport csv\nimport random\nimport warnings\nfrom pathlib import Path\nfrom typing import List, Tuple, Any\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import resnet50\nfrom sklearn.model_selection import KFold\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tqdm import tqdm\n\nimport librosa\nfrom skimage import exposure, util\nfrom skimage.transform import resize\n\nwarnings.filterwarnings(\"ignore\")\n\n# Устройство\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Используемое устройство: {DEVICE}\")\n\nNUM_CLASSES = 24\nSAMPLE_RATE = 48_000\nSEGMENT_LENGTH_SAMPLES = 10 * SAMPLE_RATE  # 10 секунд\nLEARNING_RATE = 2e-4\nNUM_EPOCHS = 20\nNUM_FOLDS = 5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:30:11.106120Z","iopub.execute_input":"2025-12-15T18:30:11.106866Z","iopub.status.idle":"2025-12-15T18:30:14.956141Z","shell.execute_reply.started":"2025-12-15T18:30:11.106840Z","shell.execute_reply":"2025-12-15T18:30:14.955481Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Вспомогательные функции","metadata":{}},{"cell_type":"code","source":"def compute_frequency_bounds(metadata_path: str) -> Tuple[int, int]:\n    \"\"\"Определяет глобальные границы частот с запасом\"\"\"\n    df = pd.read_csv(metadata_path)\n    f_min = int(df[\"f_min\"].min() * 0.9)\n    f_max = int(df[\"f_max\"].max() * 1.1)\n    return f_min, f_max\n\n\nF_MIN, F_MAX = compute_frequency_bounds(\"/kaggle/input/rfcx-species-audio-detection/train_tp.csv\")\n\n\ndef spec_to_image(spec: np.ndarray, target_shape: Tuple[int, int] = (224, 400)) -> np.ndarray:\n    \"\"\"Нормализует и масштабирует спектрограмму в uint8 изображение\"\"\"\n    spec_resized = resize(spec, target_shape, anti_aliasing=True)\n    eps = 1e-6\n    spec_norm = (spec_resized - spec_resized.mean()) / (spec_resized.std() + eps)\n    spec_min, spec_max = spec_norm.min(), spec_norm.max()\n    spec_scaled = 255 * (spec_norm - spec_min) / (spec_max - spec_min + eps)\n    return spec_scaled.astype(np.uint8)\n\n\ndef get_pretrained_resnet(num_classes: int = NUM_CLASSES) -> nn.Module:\n    \"\"\"Загружает ResNet50 и заменяет последний слой под нужное число классов\"\"\"\n    model = resnet50(weights=None)\n    weights_path = \"/kaggle/input/resnet50-weights/resnet50-weights.pth\"\n    model.load_state_dict(torch.load(weights_path, map_location=DEVICE))\n    model.fc = nn.Linear(model.fc.in_features, num_classes)\n    return model.to(DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:30:19.138922Z","iopub.execute_input":"2025-12-15T18:30:19.139488Z","iopub.status.idle":"2025-12-15T18:30:19.154773Z","shell.execute_reply.started":"2025-12-15T18:30:19.139464Z","shell.execute_reply":"2025-12-15T18:30:19.153966Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Аугментации изображений\n","metadata":{}},{"cell_type":"code","source":"class AudioImageAugmenter:\n    \"\"\"Применяет случайные аугментации к одноканальному изображению спектрограммы\"\"\"\n\n    def __init__(self):\n        self.transforms = [\n            self._horizontal_flip,\n            self._vertical_flip,\n            self._add_noise,\n            self._contrast_stretch,\n        ]\n\n    @staticmethod\n    def _horizontal_flip(img: np.ndarray) -> np.ndarray:\n        return np.stack([img[:, ::-1]] * 3, axis=0)\n\n    @staticmethod\n    def _vertical_flip(img: np.ndarray) -> np.ndarray:\n        return np.stack([img[::-1, :]] * 3, axis=0)\n\n    @staticmethod\n    def _add_noise(img: np.ndarray) -> np.ndarray:\n        noisy = util.random_noise(img, mode=\"gaussian\")\n        return np.stack([noisy] * 3, axis=0)\n\n    @staticmethod\n    def _contrast_stretch(img: np.ndarray) -> np.ndarray:\n        stretched = exposure.rescale_intensity(img)\n        return np.stack([stretched] * 3, axis=0)\n\n    def __call__(self, img: np.ndarray) -> np.ndarray:\n        \"\"\"Применяет случайную аугментацию\"\"\"\n        transform = random.choice(self.transforms)\n        return transform(img)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:30:22.782264Z","iopub.execute_input":"2025-12-15T18:30:22.782800Z","iopub.status.idle":"2025-12-15T18:30:22.789566Z","shell.execute_reply.started":"2025-12-15T18:30:22.782778Z","shell.execute_reply":"2025-12-15T18:30:22.788756Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Загрузка и предобработка аудио","metadata":{}},{"cell_type":"code","source":"def process_single_audio(\n    recording_id: str,\n    species_id: int,\n    t_min: float,\n    t_max: float,\n    audio_dir: str = \"/kaggle/input/rfcx-species-audio-detection/train\",\n    sr_target: int = SAMPLE_RATE,\n) -> Tuple[str, np.ndarray]:\n    \"\"\"Загружает аудиофайл, вырезает сегмент, строит мел-спектрограмму и конвертирует в изображение\"\"\"\n    audio_path = os.path.join(audio_dir, f\"{recording_id}.flac\")\n    wav, sr = librosa.load(audio_path, sr=None)\n\n    t_min_samp = int(t_min * sr)\n    t_max_samp = int(t_max * sr)\n    center = int(round((t_min_samp + t_max_samp) / 2))\n    start = max(center - SEGMENT_LENGTH_SAMPLES // 2, 0)\n    end = min(start + SEGMENT_LENGTH_SAMPLES, len(wav))\n    if end - start < SEGMENT_LENGTH_SAMPLES:\n        start = max(0, end - SEGMENT_LENGTH_SAMPLES)\n\n    segment = wav[start:end]\n    mel_spec = librosa.feature.melspectrogram(y=segment, sr=sr, fmin=F_MIN, fmax=F_MAX)\n    mel_db = librosa.power_to_db(mel_spec, top_db=80)\n    image = spec_to_image(mel_db)\n    return recording_id, image\n\n\ndef load_training_data(metadata_path: str) -> dict:\n    \"\"\"Параллельно обрабатывает все обучающие аудиофайлы и возвращает словарь {recording_id: image}\"\"\"\n    df = pd.read_csv(metadata_path)\n    audio_data = {}\n\n    def _process_row(idx: int):\n        row = df.iloc[idx]\n        return process_single_audio(\n            recording_id=row[\"recording_id\"],\n            species_id=row[\"species_id\"],\n            t_min=row[\"t_min\"],\n            t_max=row[\"t_max\"],\n        )\n\n    with ThreadPoolExecutor() as executor:\n        results = list(tqdm(executor.map(_process_row, range(len(df))), total=len(df)))\n\n    for rec_id, img in results:\n        audio_data[rec_id] = img\n\n    return audio_data\n\n\nAUDIO_CACHE = load_training_data(\"/kaggle/input/rfcx-species-audio-detection/train_tp.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:30:27.505751Z","iopub.execute_input":"2025-12-15T18:30:27.506037Z","iopub.status.idle":"2025-12-15T18:31:23.050428Z","shell.execute_reply.started":"2025-12-15T18:30:27.506017Z","shell.execute_reply":"2025-12-15T18:31:23.049779Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Класс датасета","metadata":{}},{"cell_type":"code","source":"class AudioImageDataset(Dataset):\n    def __init__(\n        self,\n        recording_ids: List[str],\n        labels: List[int],\n        data_split: str,  # \"train\" или \"valid\"\n        audio_cache: dict,\n        augmenter: AudioImageAugmenter = None,\n    ):\n        self.recording_ids = recording_ids\n        self.labels = labels\n        self.split = data_split\n        self.cache = audio_cache\n        self.augmenter = augmenter\n\n    def __len__(self) -> int:\n        return len(self.recording_ids)\n\n    def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:\n        rec_id = self.recording_ids[idx]\n        label = self.labels[idx]\n        img = self.cache[rec_id]\n\n        if self.split == \"train\" and self.augmenter is not None:\n            img_rgb = self.augmenter(img)\n        else:\n            img_rgb = np.stack([img] * 3, axis=0)\n\n        return torch.from_numpy(img_rgb).float(), label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:31:29.146473Z","iopub.execute_input":"2025-12-15T18:31:29.147261Z","iopub.status.idle":"2025-12-15T18:31:29.153102Z","shell.execute_reply.started":"2025-12-15T18:31:29.147234Z","shell.execute_reply":"2025-12-15T18:31:29.152231Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Обучение модели","metadata":{}},{"cell_type":"code","source":"def train_one_fold(\n    model: nn.Module,\n    train_loader: DataLoader,\n    valid_loader: DataLoader,\n    criterion: nn.Module,\n    optimizer: torch.optim.Optimizer,\n    scheduler: torch.optim.lr_scheduler._LRScheduler,\n    num_epochs: int = NUM_EPOCHS,\n) -> nn.Module:\n    best_model_state = None\n    best_acc = 0.0\n\n    for epoch in tqdm(range(1, num_epochs + 1), desc=\"Эпохи\"):\n        # Обучение\n        model.train()\n        train_losses = []\n        for inputs, targets in train_loader:\n            inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n            optimizer.zero_grad()\n            outputs = model(inputs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            train_losses.append(loss.item())\n\n        # Валидация\n        model.eval()\n        val_losses = []\n        all_preds, all_labels = [], []\n        with torch.no_grad():\n            for inputs, targets in valid_loader:\n                inputs, targets = inputs.to(DEVICE), targets.to(DEVICE)\n                outputs = model(inputs)\n                loss = criterion(outputs, targets)\n                val_losses.append(loss.item())\n                all_preds.append(outputs.cpu().numpy())\n                all_labels.append(targets.cpu().numpy())\n\n        # Оценка\n        y_true = np.concatenate(all_labels)\n        y_pred = np.concatenate(all_preds).argmax(axis=1)\n        val_acc = np.mean(y_pred == y_true)\n        avg_train_loss = np.mean(train_losses)\n        avg_val_loss = np.mean(val_losses)\n\n        print(\n            f\"Эпоха {epoch:02d} | Train Loss: {avg_train_loss:.5f} | \"\n            f\"Val Loss: {avg_val_loss:.5f} | Val Acc: {val_acc:.5f}\"\n        )\n\n        scheduler.step(avg_val_loss)\n        if val_acc > best_acc:\n            best_acc = val_acc\n            best_model_state = model.state_dict().copy()\n\n    model.load_state_dict(best_model_state)\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:31:40.814732Z","iopub.execute_input":"2025-12-15T18:31:40.814997Z","iopub.status.idle":"2025-12-15T18:31:40.822939Z","shell.execute_reply.started":"2025-12-15T18:31:40.814978Z","shell.execute_reply":"2025-12-15T18:31:40.822206Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Кросс-валидация","metadata":{}},{"cell_type":"code","source":"def run_kfold_training():\n    df = pd.read_csv(\"/kaggle/input/rfcx-species-audio-detection/train_tp.csv\")\n    recording_ids = df[\"recording_id\"].tolist()\n    labels = df[\"species_id\"].tolist()\n\n    kfold = KFold(n_splits=NUM_FOLDS, shuffle=True, random_state=563)\n\n    for fold, (train_idx, val_idx) in enumerate(kfold.split(recording_ids)):\n        print(f\"\\n=== Обучение на фолде {fold} ===\")\n\n        X_train = [recording_ids[i] for i in train_idx]\n        y_train = [labels[i] for i in train_idx]\n        X_val = [recording_ids[i] for i in val_idx]\n        y_val = [labels[i] for i in val_idx]\n\n        train_dataset = AudioImageDataset(\n            recording_ids=X_train,\n            labels=y_train,\n            data_split=\"train\",\n            audio_cache=AUDIO_CACHE,\n            augmenter=AudioImageAugmenter(),\n        )\n        val_dataset = AudioImageDataset(\n            recording_ids=X_val,\n            labels=y_val,\n            data_split=\"valid\",\n            audio_cache=AUDIO_CACHE,\n        )\n\n        train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True, drop_last=True)\n        val_loader = DataLoader(val_dataset, batch_size=8, shuffle=False, drop_last=False)\n\n        model = get_pretrained_resnet()\n        optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode=\"min\", patience=3)\n        criterion = nn.CrossEntropyLoss()\n\n        model = train_one_fold(model, train_loader, val_loader, criterion, optimizer, scheduler)\n        torch.save(model.state_dict(), f\"./model{fold}.pt\")\n\n        # Очистка памяти\n        del model, train_dataset, val_dataset, train_loader, val_loader\n\n\nrun_kfold_training()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:31:44.313038Z","iopub.execute_input":"2025-12-15T18:31:44.313581Z","iopub.status.idle":"2025-12-15T18:38:01.706822Z","shell.execute_reply.started":"2025-12-15T18:31:44.313558Z","shell.execute_reply":"2025-12-15T18:38:01.705942Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Инференс\n","metadata":{}},{"cell_type":"code","source":"def load_test_segments(test_file: str, test_dir: str = \"/kaggle/input/rfcx-species-audio-detection/test\") -> List[np.ndarray]:\n    \"\"\"Разбивает тестовый аудиофайл на 10-секундные сегменты и конвертирует в RGB-изображения\"\"\"\n    path = os.path.join(test_dir, test_file)\n    wav, sr = librosa.load(path, sr=None)\n    num_segments = int(np.ceil(len(wav) / SEGMENT_LENGTH_SAMPLES))\n    segments = []\n\n    for i in range(num_segments):\n        start = i * SEGMENT_LENGTH_SAMPLES\n        end = start + SEGMENT_LENGTH_SAMPLES\n        if end > len(wav):\n            segment = wav[-SEGMENT_LENGTH_SAMPLES:]\n        else:\n            segment = wav[start:end]\n\n        mel = librosa.feature.melspectrogram(y=segment, sr=sr, fmin=F_MIN, fmax=F_MAX)\n        mel_db = librosa.power_to_db(mel, top_db=80)\n        img = spec_to_image(mel_db)\n        img_rgb = np.stack([img] * 3, axis=0)\n        segments.append(img_rgb)\n\n    return segments\n\n\ndef ensemble_predict(\n    test_file: str,\n    models: List[nn.Module],\n) -> List[Any]:\n    segments = load_test_segments(test_file)\n    batch = torch.stack([torch.from_numpy(s).float() for s in segments]).to(DEVICE)\n\n    predictions = []\n    for model in models:\n        model.eval()\n        with torch.no_grad():\n            outputs = model(batch)\n            max_output, _ = torch.max(outputs, dim=0)  # максимум по сегментам\n            predictions.append(max_output.cpu())\n\n    mean_prediction = torch.mean(torch.stack(predictions), dim=0).tolist()\n    recording_id = Path(test_file).stem\n    return [recording_id] + mean_prediction","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:39:17.999431Z","iopub.execute_input":"2025-12-15T18:39:17.999693Z","iopub.status.idle":"2025-12-15T18:39:18.007403Z","shell.execute_reply.started":"2025-12-15T18:39:17.999676Z","shell.execute_reply":"2025-12-15T18:39:18.006551Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Работа с тестовыми данными и формирование решения","metadata":{}},{"cell_type":"code","source":"def save_submission_file(rows: List[List], filename: str = \"submission.csv\"):\n    header = [\"recording_id\"] + [f\"s{i}\" for i in range(NUM_CLASSES)]\n    with open(filename, \"w\", newline=\"\") as f:\n        writer = csv.writer(f)\n        writer.writerow(header)\n        writer.writerows(rows)\n\ndef generate_and_save_submission(test_dir: str = \"/kaggle/input/rfcx-species-audio-detection/test\"):\n    test_files = [f for f in os.listdir(test_dir) if f.endswith(\".flac\")]\n    print(f\"Найдено тестовых файлов: {len(test_files)}\")\n\n    # Загрузка ансамбля\n    models = []\n    for fold in range(NUM_FOLDS):\n        model = get_pretrained_resnet()\n        model.load_state_dict(torch.load(f\"./model{fold}.pt\", map_location=DEVICE))\n        models.append(model)\n        os.remove(f\"./model{fold}.pt\")  # очистка\n\n    if torch.cuda.is_available():\n        models = [m.cuda() for m in models]\n\n    # Предсказание\n    results = []\n    with ThreadPoolExecutor(max_workers=4) as executor:\n        futures = [executor.submit(ensemble_predict, f, models) for f in test_files]\n        for future in tqdm(futures, desc=\"Инференс\"):\n            results.append(future.result())\n\n    save_submission_file(results)\n\n\ngenerate_and_save_submission()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-15T18:39:26.007246Z","iopub.execute_input":"2025-12-15T18:39:26.007835Z","iopub.status.idle":"2025-12-15T18:44:27.968545Z","shell.execute_reply.started":"2025-12-15T18:39:26.007808Z","shell.execute_reply":"2025-12-15T18:44:27.967846Z"}},"outputs":[],"execution_count":null}]}