{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":25954,"databundleVersionId":2091745,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":1297722,"sourceType":"datasetVersion","datasetId":750498},{"sourceId":2130303,"sourceType":"datasetVersion","datasetId":1278322}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# BirdCLEF 2021 — детекция птиц по аудио с помощью ResNet50\n\n\nВ этой работе мы решаем задачу из соревнования **BirdCLEF 2021**:\n\n> по длинным звуковым записям (soundscapes) предсказать,\n> какие виды птиц поют на каждом 5-секундном отрезке.\n\nМы **не обучаем модель с нуля** (это долго и дорого),\nа используем заранее обученный **ResNet50**, натренированный на коротких аудиоклипах.  \nНаша задача — аккуратно:\n\n1. Преобразовать звук → мел-спектрограмму → «картинку»\n2. Прогнать эти «картинки» через ResNet50\n3. Собрать предсказания в формате `submission.csv`\n4. Если мы в offline-режиме (используем `train_soundscapes`) — оценить качество.","metadata":{}},{"cell_type":"code","source":"import os\nimport math\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport librosa as lb\nimport soundfile as sf\n\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset\nimport torchvision.models as models\n\nfrom tqdm.notebook import tqdm\nfrom matplotlib import pyplot as plt\n\nfrom sklearn.metrics import f1_score\n\nprint(\"Torch:\", torch.__version__)\n\n# Основные константы\nNUM_CLASSES = 397          # число видов птиц в BirdCLEF 2021\nSR = 32000                 # частота дискретизации\nDURATION = 5               # длина окна в секундах\nTHRESH = 0.25              # порог для выбора видов\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"DEVICE:\", DEVICE)\n\nDATA_ROOT = Path(\"../input/birdclef-2021\")\n\nTEST_AUDIO_ROOT = DATA_ROOT / \"test_soundscapes\"\nSAMPLE_SUB_PATH = DATA_ROOT / \"sample_submission.csv\"\nTARGET_PATH = None\n\n# Если в тесте нет файлов, работаем в offline-режиме по train_soundscapes\nif not len(list(TEST_AUDIO_ROOT.glob(\"*.ogg\"))):\n    TEST_AUDIO_ROOT = DATA_ROOT / \"train_soundscapes\"\n    SAMPLE_SUB_PATH = None\n    TARGET_PATH = DATA_ROOT / \"train_soundscape_labels.csv\"\n\nprint(\"Audio root:\", TEST_AUDIO_ROOT)\nprint(\"Sample submission:\", SAMPLE_SUB_PATH)\nprint(\"Train soundscape labels:\", TARGET_PATH)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:37.682322Z","iopub.execute_input":"2025-12-03T19:33:37.682589Z","iopub.status.idle":"2025-12-03T19:33:41.888717Z","shell.execute_reply.started":"2025-12-03T19:33:37.682571Z","shell.execute_reply":"2025-12-03T19:33:41.888032Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls /kaggle/input/resnest50-fast-package/resnest-0.0.6b20200701/resnest\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:41.889827Z","iopub.execute_input":"2025-12-03T19:33:41.890229Z","iopub.status.idle":"2025-12-03T19:33:42.027036Z","shell.execute_reply.started":"2025-12-03T19:33:41.890210Z","shell.execute_reply":"2025-12-03T19:33:42.026342Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\n\n# добавляем корень пакета в sys.path\nsys.path.append(\"/kaggle/input/resnest50-fast-package/resnest-0.0.6b20200701\")\nsys.path.append(\"/kaggle/input/resnest50-fast-package/resnest-0.0.6b20200701/resnest\")\n\nfrom resnest.torch import resnest50\n\nprint(\"ResNeSt импортирован ок!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.027912Z","iopub.execute_input":"2025-12-03T19:33:42.028152Z","iopub.status.idle":"2025-12-03T19:33:42.051934Z","shell.execute_reply.started":"2025-12-03T19:33:42.028123Z","shell.execute_reply":"2025-12-03T19:33:42.051410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Соберём для каждого site список видов, которые реально встречались в train_soundscapes\nSITE_SPECIES = {}\n\nif TARGET_PATH is not None and TARGET_PATH.exists():\n    labels_df_site = pd.read_csv(TARGET_PATH)\n\n    for row in labels_df_site.itertuples(index=False):\n        site = row.site\n        birds = row.birds\n        if isinstance(birds, str) and birds != \"nocall\":\n            species = birds.split()\n            if site not in SITE_SPECIES:\n                SITE_SPECIES[site] = set()\n            SITE_SPECIES[site].update(species)\n\n    print(\"Виды по сайтам:\")\n    for s, sp in SITE_SPECIES.items():\n        print(f\"{s}: {len(sp)} видов\")\nelse:\n    print(\"train_soundscape_labels.csv не найден, SITE_SPECIES пустой\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.052695Z","iopub.execute_input":"2025-12-03T19:33:42.052884Z","iopub.status.idle":"2025-12-03T19:33:42.075634Z","shell.execute_reply.started":"2025-12-03T19:33:42.052868Z","shell.execute_reply":"2025-12-03T19:33:42.075017Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Данные: soundscapes\n\nВ соревновании есть два типа данных:\n\n- **Короткие клипы** (`train_short_audio`) — по ним обычно обучают модель\n- **Длинные soundscape-записи** (`train_soundscapes`, `test_soundscapes`) — на них нужно делать предсказания для сабмита\n\nВ этой работе мы будем:\n\n- Читать **длинные .ogg** из `*_soundscapes`\n- Резать каждую запись на окна по **5 секунд**\n- Для каждого окна считать мел-спектрограмму и подавать в ResNet18\n","metadata":{}},{"cell_type":"code","source":"# Собираем таблицу с файлами soundscapes\ndata = pd.DataFrame(\n    [\n        (path.stem, *path.stem.split(\"_\"), path)\n        for path in TEST_AUDIO_ROOT.glob(\"*.ogg\")\n    ],\n    columns=[\"filename\", \"id\", \"site\", \"date\", \"filepath\"],\n)\n\nprint(\"Число файлов soundscapes:\", data.shape[0])\ndata.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.077222Z","iopub.execute_input":"2025-12-03T19:33:42.077631Z","iopub.status.idle":"2025-12-03T19:33:42.092699Z","shell.execute_reply.started":"2025-12-03T19:33:42.077614Z","shell.execute_reply":"2025-12-03T19:33:42.092085Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Аудио → Mel-спектрограмма → «картинка»\n\nМодель ResNet18 ожидает на вход **трёхканальные изображения** (`[3, H, W]`),\nа у нас — 1D-аудио.\n\nПайплайн такой:\n\n1. Загружаем аудио (`soundfile` / `librosa`)\n2. При необходимости **ресемплим** до 32 kHz\n3. Режем длинную дорожку на окна по 5 секунд\n4. Для каждого окна:\n   - считаем **mel-спектрограмму** (128 мел-фильтров)\n   - переводим в **децибелы** (`power_to_db`)\n   - нормируем и растягиваем в диапазон [0, 255]\n   - дублируем канал → получаем `[3, H, W]`\n\nГотовые «картинки» уже можно подавать в ResNet18.\n","metadata":{}},{"cell_type":"code","source":"class MelSpecComputer:\n    \"\"\"\n    Обёртка над librosa.feature.melspectrogram\n    с более удобной настройкой параметров.\n    \"\"\"\n    def __init__(self, sr, n_mels, fmin, fmax, **kwargs):\n        self.sr = sr\n        self.n_mels = n_mels\n        self.fmin = fmin\n        self.fmax = fmax\n\n        kwargs[\"n_fft\"] = kwargs.get(\"n_fft\", self.sr // 10)\n        kwargs[\"hop_length\"] = kwargs.get(\"hop_length\", self.sr // 40)\n        self.kwargs = kwargs\n\n    def __call__(self, y):\n        melspec = lb.feature.melspectrogram(\n            y=y,\n            sr=self.sr,\n            n_mels=self.n_mels,\n            fmin=self.fmin,\n            fmax=self.fmax,\n            **self.kwargs,\n        )\n        melspec = lb.power_to_db(melspec).astype(np.float32)\n        return melspec\n\n\ndef mono_to_color(X, eps=1e-6, mean=None, std=None):\n    \"\"\"\n    Переводим мел-спектрограмму в \"картинку\":\n    нормируем, растягиваем в [0,255], приводим к uint8.\n    \"\"\"\n    mean = mean or X.mean()\n    std = std or X.std()\n    X = (X - mean) / (std + eps)\n\n    _min, _max = X.min(), X.max()\n    if (_max - _min) > eps:\n        V = np.clip(X, _min, _max)\n        V = 255 * (V - _min) / (_max - _min)\n        V = V.astype(np.uint8)\n    else:\n        V = np.zeros_like(X, dtype=np.uint8)\n\n    return V\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.093299Z","iopub.execute_input":"2025-12-03T19:33:42.093496Z","iopub.status.idle":"2025-12-03T19:33:42.101924Z","shell.execute_reply.started":"2025-12-03T19:33:42.093481Z","shell.execute_reply":"2025-12-03T19:33:42.101256Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFDataset(Dataset):\n    \"\"\"\n    Датасет для длинных soundscape-записей.\n    На каждый файл возвращает массив \"картинок\" для всех 5-секундных окон:\n    (num_windows, 3, n_mels, time)\n    \"\"\"\n    def __init__(\n        self,\n        data,\n        sr=SR,\n        n_mels=128,\n        fmin=0,\n        fmax=None,\n        duration=DURATION,\n        step=None,\n        res_type=\"kaiser_fast\",\n        resample=True,\n    ):\n        self.data = data\n\n        self.sr = sr\n        self.n_mels = n_mels\n        self.fmin = fmin\n        self.fmax = fmax or self.sr // 2\n\n        self.duration = duration\n        self.audio_length = int(self.duration * self.sr)\n        self.step = step or self.audio_length  # сдвигаем ровно на окно\n\n        self.res_type = res_type\n        self.resample = resample\n\n        self.mel_spec_computer = MelSpecComputer(\n            sr=self.sr,\n            n_mels=self.n_mels,\n            fmin=self.fmin,\n            fmax=self.fmax,\n        )\n\n    def __len__(self):\n        return len(self.data)\n\n    @staticmethod\n    def normalize(image):\n        image = image.astype(\"float32\", copy=False) / 255.0\n        image = np.stack([image, image, image])  # [3,H,W]\n        return image\n\n    def audio_to_image(self, audio):\n        melspec = self.mel_spec_computer(audio)\n        image = mono_to_color(melspec)\n        image = self.normalize(image)\n        return image\n\n    def read_file(self, filepath: Path):\n        audio, orig_sr = sf.read(filepath, dtype=\"float32\")\n\n        if self.resample and orig_sr != self.sr:\n            audio = lb.resample(audio, orig_sr, self.sr, res_type=self.res_type)\n\n        audios = []\n        # нарезаем по 5 секунд\n        for i in range(self.audio_length, len(audio) + self.step, self.step):\n            start = max(0, i - self.audio_length)\n            end = start + self.audio_length\n            audios.append(audio[start:end])\n\n        # если последний кусок получился слишком коротким — выбрасываем\n        if len(audios) > 0 and len(audios[-1]) < self.audio_length:\n            audios = audios[:-1]\n\n        images = [self.audio_to_image(a) for a in audios]\n        if len(images) == 0:\n            # очень короткий файл — паддим тишиной\n            images = [self.audio_to_image(np.zeros(self.audio_length, dtype=np.float32))]\n        images = np.stack(images)\n        return images\n\n    def __getitem__(self, idx):\n        return self.read_file(self.data.loc[idx, \"filepath\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.102660Z","iopub.execute_input":"2025-12-03T19:33:42.102878Z","iopub.status.idle":"2025-12-03T19:33:42.120053Z","shell.execute_reply.started":"2025-12-03T19:33:42.102852Z","shell.execute_reply":"2025-12-03T19:33:42.119463Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_data = BirdCLEFDataset(data=data)\n\nprint(\"Всего файлов:\", len(test_data))\nsample_batch = test_data[0]\nprint(\"Форма для первого файла:\", sample_batch.shape)  # (num_windows, 3, 128, T)\n\n# Визуализируем одну картинку (один 5-сек отрезок)\nimg = sample_batch[0]  # (3, H, W)\nplt.figure(figsize=(10, 4))\nplt.title(\"Пример mel-спектрограммы (как картинка)\")\nplt.imshow(np.transpose(img, (1, 2, 0)))\nplt.axis(\"off\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:42.120869Z","iopub.execute_input":"2025-12-03T19:33:42.121049Z","iopub.status.idle":"2025-12-03T19:33:46.053588Z","shell.execute_reply.started":"2025-12-03T19:33:42.121034Z","shell.execute_reply":"2025-12-03T19:33:46.052905Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Модель ResNet18, обученная на BirdCLEF\n\nВместо того, чтобы обучать модель с нуля, мы:\n\n- берём **ResNet18** из `torchvision.models`\n- заменяем последний полносвязный слой на `Linear(..., 397)`\n- загружаем **готовый чекпоинт**, предварительно обученный на `train_short_audio`\n\nДальше мы просто используем сеть как мощный feature extractor + классификатор:\nкаждый 5-секундный кадр мел-спектрограммы превращается в предсказания по 397 видам птиц.\n","metadata":{}},{"cell_type":"code","source":"import gc, torch\n\ngc.collect()\ntorch.cuda.empty_cache()\nprint(\"CUDA memory cleaned\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:46.054365Z","iopub.execute_input":"2025-12-03T19:33:46.054750Z","iopub.status.idle":"2025-12-03T19:33:46.280545Z","shell.execute_reply.started":"2025-12-03T19:33:46.054732Z","shell.execute_reply":"2025-12-03T19:33:46.279821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Метаданные train, чтобы восстановить порядок классов\ndf_train = pd.read_csv(DATA_ROOT / \"train_metadata.csv\")\n\nLABEL_IDS = {label: label_id for label_id, label in enumerate(sorted(df_train[\"primary_label\"].unique()))}\nINV_LABEL_IDS = {v: k for k, v in LABEL_IDS.items()}\nNUM_CLASSES = len(LABEL_IDS)\nprint(\"Число классов:\", NUM_CLASSES)\n\n\ndef load_net_resnest(checkpoint_path, num_classes=NUM_CLASSES):\n    \"\"\"\n    Загружаем ResNeSt50, обученный на BirdCLEF\n    (чекпоинт из kkiller-birdclef-models-public).\n    \"\"\"\n    net = resnest50(pretrained=False)\n    net.fc = nn.Linear(net.fc.in_features, num_classes)\n\n    state = torch.load(checkpoint_path, map_location=\"cpu\")\n\n    # иногда веса сохраняются с префиксом \"model.\"\n    for k in list(state.keys()):\n        if k.startswith(\"model.\"):\n            state[k[6:]] = state.pop(k)\n\n    net.load_state_dict(state, strict=True)\n    net = net.to(DEVICE)\n    net.eval()\n    return net\n\n\n\ncheckpoint_paths = [\n    Path(\n        \"/kaggle/input/kkiller-birdclef-models-public\"\n        \"/birdclef_resnest50_fold0_epoch_10_f1_val_06471_20210417161101.pth\"\n    )\n]\n\nnets = [load_net_resnest(path) for path in checkpoint_paths]\nprint(f\"Моделей в ансамбле: {len(nets)}\")\nprint(nets[0].fc)\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:46.281301Z","iopub.execute_input":"2025-12-03T19:33:46.281548Z","iopub.status.idle":"2025-12-03T19:33:47.306591Z","shell.execute_reply.started":"2025-12-03T19:33:46.281521Z","shell.execute_reply":"2025-12-03T19:33:47.305907Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Постобработка: какие птицы \"поют\" в кадре\n\nResNet выдаёт на каждый кадр вектор из 397 логитов. Для интерпретации:\n\n1. Применяем `sigmoid` → получаем вероятности по каждой птице\n2. Выбираем виды, у которых `p > THRESH` (например, 0.25)\n3. Берём индексы этих видов, переводим обратно в имена через `INV_LABEL_IDS`\n4. Если ни одной птицы не набрало порог → пишем `\"nocall\"`\n\nТакже удобно иметь функцию, которая возвращает **списки id-шников** (для анализа),\nи функцию, которая сразу формирует строку `\"sp1 sp2 sp3\"` для submission.\n","metadata":{}},{"cell_type":"code","source":"@torch.no_grad()\ndef get_thresh_preds(out: torch.Tensor, thresh: float = None):\n    \"\"\"\n    out: [N, num_classes] после sigmoid\n    Возвращает список списков индексов классов, прошедших порог.\n    \"\"\"\n    thresh = thresh or THRESH\n    # сортируем по вероятности по убыванию\n    sorted_idx = (-out).argsort(1)  # [N, num_classes]\n    num_above = (out > thresh).sum(1)  # [N]\n\n    preds = []\n    for row_idx, n in zip(sorted_idx, num_above):\n        idxs = row_idx[:n].cpu().numpy().tolist()\n        preds.append(idxs)\n    return preds\n\n\ndef get_bird_names(preds, site=None):\n    bird_names = []\n    for p in preds:\n        if not p:\n            bird_names.append(\"nocall\")\n        else:\n            bird_names.append(\" \".join(INV_LABEL_IDS[i] for i in p))\n    return bird_names\n\n\n\n@torch.no_grad()\ndef predict(nets, dataset, meta_df=None, return_names=True, batch_size=16):\n    \"\"\"\n    nets: список моделей\n    dataset: BirdCLEFDataset\n    meta_df: DataFrame с колонкой 'site' (наш data)\n    return_names:\n      - True  -> строки с названиями видов / 'nocall'\n      - False -> сырые вероятности (numpy) [num_windows, num_classes]\n    batch_size: сколько 5-сек окон обрабатывать за раз (важно для CUDA OOM)\n    \"\"\"\n    all_preds = []\n\n    for idx in tqdm(range(len(dataset)), desc=\"Inference over soundscapes\"):\n        # numpy-массив для текущего файла: [num_windows, 3, H, W]\n        arr = dataset[idx]\n        num_windows = arr.shape[0]\n\n        # собираем предсказания по батчам\n        probs_chunks = []\n\n        for start in range(0, num_windows, batch_size):\n            end = min(start + batch_size, num_windows)\n            batch_np = arr[start:end]                          # [B, 3, H, W]\n            xb = torch.from_numpy(batch_np).to(DEVICE)\n\n            # усредняем по ансамблю\n            batch_probs = torch.zeros(\n                (end - start, NUM_CLASSES), device=DEVICE, dtype=torch.float32\n            )\n            for net in nets:\n                logits = net(xb)                               # [B, C]\n                batch_probs += torch.sigmoid(logits)\n            batch_probs /= len(nets)\n\n            probs_chunks.append(batch_probs.cpu())\n\n            # чистим за собой\n            del xb, logits, batch_probs\n            torch.cuda.empty_cache()\n\n        # склеиваем все батчи в [num_windows, num_classes]\n        full_probs = torch.cat(probs_chunks, dim=0)\n\n        if return_names:\n            site = meta_df.iloc[idx].site if meta_df is not None else None\n            idx_preds = get_thresh_preds(full_probs)           # использует THRESH\n            window_preds = get_bird_names(idx_preds, site=site)\n        else:\n            window_preds = full_probs.numpy()\n\n        all_preds.append(window_preds)\n\n        # ещё немного уборки\n        del probs_chunks, full_probs, window_preds\n        torch.cuda.empty_cache()\n\n    return all_preds\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:47.307354Z","iopub.execute_input":"2025-12-03T19:33:47.307677Z","iopub.status.idle":"2025-12-03T19:33:47.317373Z","shell.execute_reply.started":"2025-12-03T19:33:47.307658Z","shell.execute_reply":"2025-12-03T19:33:47.316607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Получаем предсказания: для каждого файла список строк по 5-сек окнами\npreds = predict(nets, test_data, meta_df=data, return_names=True)\n\nprint(f\"Сколько файлов обработано: {len(preds)}\")\nprint(\"Для первого файла количество окон:\", len(preds[0]))\nprint(\"Первые 10 окон и предсказанные птицы:\")\nfor i, p in enumerate(preds[0][:10], start=1):\n    print(f\"{i*DURATION- DURATION:>3}-{i*DURATION:>3} сек: {p}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:33:47.318235Z","iopub.execute_input":"2025-12-03T19:33:47.318497Z","iopub.status.idle":"2025-12-03T19:34:40.667293Z","shell.execute_reply.started":"2025-12-03T19:33:47.318473Z","shell.execute_reply":"2025-12-03T19:34:40.666383Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Формируем submission.csv\n\nСабмит должен выглядеть так:\n\n- `row_id` — строка вида `audio_id_site_seconds`\n- `birds` — строка с названиями видов через пробел или `\"nocall\"`\n\nТак как мы режем каждый файл на шаги по 5 секунд, то для файла `id=7019`, `site=COR`:\n\n- `7019_COR_5`, `7019_COR_10`, ..., `7019_COR_600`\n\nЕсли Kaggle нам дал `sample_submission.csv`, мы просто **совмещаем** наши предсказания с его `row_id`.  \nЕсли работаем в offline-режиме на `train_soundscapes`, сами формируем все `row_id`.\n","metadata":{}},{"cell_type":"code","source":"def preds_as_df(data, preds, duration=DURATION):\n    \"\"\"\n    data: DataFrame с колонками id, site\n    preds: список списков строк ('nocall' или 'sp1 sp2 ...') для каждого файла\n    \"\"\"\n    rows = {\n        \"row_id\": [],\n        \"birds\": [],\n    }\n\n    for row, file_preds in zip(data.itertuples(index=False), preds):\n        # row.id, row.site\n        for i, birds in enumerate(file_preds, start=1):\n            second = i * duration\n            row_id = f\"{row.id}_{row.site}_{second}\"\n            rows[\"row_id\"].append(row_id)\n            rows[\"birds\"].append(birds)\n\n    sub = pd.DataFrame(rows)\n\n    # если есть sample_submission.csv, то подстраиваемся под его row_id\n    if SAMPLE_SUB_PATH is not None and SAMPLE_SUB_PATH.exists():\n        sample_sub = pd.read_csv(SAMPLE_SUB_PATH, usecols=[\"row_id\"])\n        sub = sample_sub.merge(sub, on=\"row_id\", how=\"left\")\n        sub[\"birds\"] = sub[\"birds\"].fillna(\"nocall\")\n\n    return sub\n\n\nsubmission = preds_as_df(data, preds, duration=DURATION)\nprint(submission.shape)\nsubmission.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:34:40.667901Z","iopub.execute_input":"2025-12-03T19:34:40.668135Z","iopub.status.idle":"2025-12-03T19:34:40.682670Z","shell.execute_reply.started":"2025-12-03T19:34:40.668115Z","shell.execute_reply":"2025-12-03T19:34:40.681874Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.to_csv(\"submission.csv\", index=False)\nprint(\"submission.csv сохранён!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:34:40.685165Z","iopub.execute_input":"2025-12-03T19:34:40.685439Z","iopub.status.idle":"2025-12-03T19:34:40.704770Z","shell.execute_reply.started":"2025-12-03T19:34:40.685421Z","shell.execute_reply":"2025-12-03T19:34:40.704064Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Оценка качества (offline)\n\nЕсли тестового набора нет и мы работаем на `train_soundscapes`,  \nKaggle даёт нам файл `train_soundscape_labels.csv` с колонками:\n\n- `row_id`\n- `birds` (строка с названиями видов или `\"nocall\"`)\n\nМы можем:\n\n1. Сопоставить наши предсказания с этими метками\n2. Посчитать **row-wise micro F1** — метрику, очень похожую на ту, что использовалась в соревновании\n\nЭто не официальный скор, но даёт хорошее представление о том,\nнасколько адекватно работает модель.\n","metadata":{}},{"cell_type":"code","source":"def rowwise_micro_f1(true_birds, pred_birds):\n    \"\"\"\n    Приблизительный row-wise micro F1:\n    для каждой строки сравниваем множества истинных и предсказанных видов.\n    \"\"\"\n    tp = fp = fn = 0\n\n    for yt, yp in zip(true_birds, pred_birds):\n        # NaN -> nocall\n        if isinstance(yt, float) and math.isnan(yt):\n            yt = \"nocall\"\n        if isinstance(yp, float) and math.isnan(yp):\n            yp = \"nocall\"\n\n        true_set = set([] if yt == \"nocall\" else yt.split())\n        pred_set = set([] if yp == \"nocall\" else yp.split())\n\n        tp += len(true_set & pred_set)\n        fp += len(pred_set - true_set)\n        fn += len(true_set - pred_set)\n\n    if tp + fp + fn == 0:\n        return 0.0\n\n    precision = tp / (tp + fp + 1e-8)\n    recall = tp / (tp + fn + 1e-8)\n    if precision + recall == 0:\n        return 0.0\n    return 2 * precision * recall / (precision + recall)\n\n\nif TARGET_PATH is not None and TARGET_PATH.exists():\n    labels_df = pd.read_csv(TARGET_PATH)\n    # совмещаем по row_id\n    merged = labels_df[[\"row_id\", \"birds\"]].merge(\n        submission, on=\"row_id\", how=\"left\", suffixes=(\"_true\", \"_pred\")\n    )\n\n    score = rowwise_micro_f1(\n        merged[\"birds_true\"].tolist(),\n        merged[\"birds_pred\"].fillna(\"nocall\").tolist(),\n    )\n    print(f\"Row-wise micro F1 на train_soundscapes (offline): {score:.4f}\")\nelse:\n    print(\"Нет train_soundscape_labels.csv — offline-оценка недоступна.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-03T19:34:40.705531Z","iopub.execute_input":"2025-12-03T19:34:40.705774Z","iopub.status.idle":"2025-12-03T19:34:40.727161Z","shell.execute_reply.started":"2025-12-03T19:34:40.705758Z","shell.execute_reply":"2025-12-03T19:34:40.726472Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Выводы\n\nВ этой лабораторной работе мы:\n\n1. Разобрались с устройством датасета BirdCLEF 2021:\n   - короткие клипы (`train_short_audio`),\n   - длинные soundscapes (`*_soundscapes`),\n   - формат сабмита (`row_id`, `birds`).\n\n2. Построили понятный аудио-пайплайн:\n   - ресемплирование звука до 32 kHz,\n   - нарезка на 5-секундные окна,\n   - вычисление mel-спектрограмм,\n   - нормализация и перевод в трёхканальные изображения.\n\n3. Использовали заранее обученную модель **ResNet18**:\n   - заменили последний слой на 397 классов,\n   - загрузили веса, обученные на `train_short_audio`,\n   - получили предсказания для каждого 5-секундного окна.\n\n4. Собрали предсказания в `submission.csv`:\n   - корректно сформировали `row_id`,\n   - вывели названия видов или `nocall`.\n\n5. В offline-режиме посчитали **row-wise micro F1** на `train_soundscapes`,\n   чтобы оценить, насколько адекватно работает модель.\n\nТакой подход позволяет:\n- не тратить часы на обучение в рамках лабы,\n- но при этом пройти **весь реальный ML-пайплайн**:\n  от сырых аудио до сабмита и метрики качества.\n","metadata":{}}]}