{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.14","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":25954,"databundleVersionId":2091745,"sourceType":"competition"},{"sourceId":1297722,"sourceType":"datasetVersion","datasetId":750498},{"sourceId":2130303,"sourceType":"datasetVersion","datasetId":1278322}],"dockerImageVersionId":30787,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Установка ResNet\n\nВ решении будет использоваться преобученная модель ResNet. Используется реализация resnest на pytorch.\n\n*Так как авторы соревнования* **запретили** использование интернета, то реализация будет использоваться из входных данных на kaggle. Для того этого необходимо установить python пакет из локальной директории. Так как папка `input` доступна только на чтение, то предварительно папка копируется в текущую","metadata":{}},{"cell_type":"code","source":"import shutil\nshutil.copytree('../input/resnest50-fast-package/resnest-0.0.6b20200701/resnest', 'resnet', dirs_exist_ok=True) \n!pip install \"./resnet\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:13:20.561819Z","iopub.execute_input":"2024-11-25T21:13:20.562080Z","iopub.status.idle":"2024-11-25T21:14:04.239541Z","shell.execute_reply.started":"2024-11-25T21:13:20.562054Z","shell.execute_reply":"2024-11-25T21:14:04.238703Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Зависимости\n\n* `os` -- поиск по директориям\n* `math` -- для округления вверх (функция `ceil`), нужно один раз\n* `pandas` и `numpy` -- работа с массивами и csv\n* `librosa` -- обработка звуковых данных\n* `torch` -- для запуска преобученной модели resnet\n* `matplotlib` -- построение графиков, нужно один раз, чтобы посмотреть на изображения mel-спектрограммы\n* `resnest` -- реализация resnet\n* `sklearn` -- кодирование меток","metadata":{}},{"cell_type":"code","source":"import os\nimport math\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport matplotlib.pyplot as plt\nfrom resnest.torch import resnest50\nfrom sklearn.preprocessing import LabelEncoder","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:04.241183Z","iopub.execute_input":"2024-11-25T21:14:04.241468Z","iopub.status.idle":"2024-11-25T21:14:07.725135Z","shell.execute_reply.started":"2024-11-25T21:14:04.241439Z","shell.execute_reply":"2024-11-25T21:14:07.724444Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Получение входных данных\n\nПри отправке решения информация о звуковых данных для предсказания будет лежать в файле `test.csv`,\nа сами звуковые данные в `test_soundscapes`. Если это пробный запуск, то в файле `test.csv` будет\nвсего 3 записи. В этом случае обработаются train данные, чтобы проверить корректность запуска pipeline","metadata":{}},{"cell_type":"code","source":"TEST_DIR = \"../input/birdclef-2021/test_soundscapes\"\nTRAIN_DIR = \"../input/birdclef-2021/train_soundscapes\"\n\ndf = pd.read_csv(\"../input/birdclef-2021/test.csv\")\naudio_dir = TEST_DIR\nif len(df) < 5:\n    # It is draft run, use train csv\n    df = pd.read_csv(\"../input/birdclef-2021/train_soundscape_labels.csv\")\n    audio_dir = TRAIN_DIR\n\nprint(df.shape)\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:07.726168Z","iopub.execute_input":"2024-11-25T21:14:07.726682Z","iopub.status.idle":"2024-11-25T21:14:07.769117Z","shell.execute_reply.started":"2024-11-25T21:14:07.726635Z","shell.execute_reply":"2024-11-25T21:14:07.768284Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Функции чтения аудиофайлов\n\n## Функция `get_audio`\n\nПо переданным сайт (`dataset`) и уникальному номеру (`idn`) ищёт в папке с ауидофайлами нужный и считывает его в оперативную память\nПри необходимости изменяет частоту на желаемую\n\n## Фунцкция `get_audio_part`\n\nАналогично функции `get_audio` только дополнительно вырезает кусок длиною в 5 секунд\n\n## Кэширование\n\nДля переиспользования данных, все возвращаемые данные складываются в кэш и затем берутся оттуда","metadata":{}},{"cell_type":"code","source":"# id_dataset -> (wave_form, rate)\ncache_audio = {}\n# id_dataset_seconds -> (wave_form part, rate)\ncache_splitted = {}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:07.770742Z","iopub.execute_input":"2024-11-25T21:14:07.771001Z","iopub.status.idle":"2024-11-25T21:14:07.774812Z","shell.execute_reply.started":"2024-11-25T21:14:07.770975Z","shell.execute_reply":"2024-11-25T21:14:07.774004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_audio(dataset: str, idn: str, desired_sr=None):\n    key = f\"{idn}_{dataset}\"\n    if key in cache_audio:\n        return cache_audio[key]\n    p = None\n    for x in os.listdir(audio_dir):\n        if x.startswith(key):\n            p = f\"{audio_dir}/{x}\"\n            break\n    if p is None:\n        raise ValueError(f\"Cannot find {key} in audio dir\")\n    waveform, sample_rate = librosa.load(p, sr=None, res_type='kaiser_fast')\n    # If audio has another sample rate, then convert it to desired sample rate\n    if desired_sr is not None and desired_sr != sample_rate:\n        waveform = librosa.resample(waveform, sample_rate, desired_sr, res_type='kaiser_fast')\n        sample_rate = desired_sr\n    cache_audio[key] = (waveform, sample_rate)\n    return (waveform, sample_rate)\n\n\ndef get_audio_part(dataset: str, idn: str, second: str, duration: int = 5, desired_sr=None):\n    key = f\"{idn}_{dataset}_{second}\"\n    if key in cache_splitted:\n        return cache_splitted[key]\n    wave, rate = get_audio(dataset, idn, desired_sr)\n    end = int(second) * rate\n    start = end - duration * rate\n    waveform_part = wave[start:end]\n    cache_splitted[key] = (waveform_part, rate)\n    return (waveform_part, rate)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:07.775862Z","iopub.execute_input":"2024-11-25T21:14:07.776114Z","iopub.status.idle":"2024-11-25T21:14:07.787162Z","shell.execute_reply.started":"2024-11-25T21:14:07.776089Z","shell.execute_reply":"2024-11-25T21:14:07.786442Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Загрузчик данных\n\n## Класс `CustomDataset`\n\nПо индексу получает считывает аудиоданные и преобразует их в изображение, которое принимает на вход преобучения модель\n\n## Функция `melspectrogram`\n\nВычисляет mel-спекторграмму по полученным данным","metadata":{}},{"cell_type":"code","source":"def melspectrogram(w, r):\n    return librosa.power_to_db(\n        librosa.feature.melspectrogram(\n            y=w, sr=r, n_mels=128, fmin=0, fmax=r // 2,\n            n_fft=r // 10, hop_length=r // 40)\n    ).astype(np.float32)\n\ndef to_image(X):\n    eps = 1e-6\n    mean = X.mean()\n    std = X.std()\n    X = (X - mean) / (std + eps)\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    return V\n\ndef to_norm_rgb(image):\n    return np.stack([image.astype(np.float32) / 255.0] * 3)\n\nclass CustomDataset(Dataset):\n    def __init__(self, data):\n        self.data = data\n        self.sr = 32000\n        self.duration = 5  # in seconds\n        self.cache = {}\n    \n    def __len__(self):\n        return len(self.data)\n    \n    def audio_to_image(self, audio):\n        return to_norm_rgb(\n            to_image(\n                melspectrogram(audio, self.sr)\n            )\n        )\n    \n    def __getitem__(self, idx):\n        if idx in self.cache:\n            return self.cache[idx]\n\n        raw_id = self.data.loc[idx, \"row_id\"]\n        idn, dataset, second = raw_id.split(\"_\")[:3]\n        wave, sr = get_audio_part(dataset, idn, int(second), duration=self.duration, desired_sr=self.sr)\n        self.cache[idx] = self.audio_to_image(wave)\n        return self.cache[idx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:07.788116Z","iopub.execute_input":"2024-11-25T21:14:07.788450Z","iopub.status.idle":"2024-11-25T21:14:07.801241Z","shell.execute_reply.started":"2024-11-25T21:14:07.788401Z","shell.execute_reply":"2024-11-25T21:14:07.800394Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Проверим работу созданного загрузчика данных и посмотрим на изображения. Получаются трехканальные нормализованные изображения 128 mels на 313. Видно наличие шума и вспелски частот -- общий шум леса, например, шелестение листвы.","metadata":{}},{"cell_type":"code","source":"test_data = CustomDataset(df)\ntest_item = test_data[1]\nprint(test_item.shape)\n\ncount = 9\nfig, axs = plt.subplots(count, 1, figsize=(10, 20))\nfor i in range(count):\n    axs[i].imshow(np.transpose(test_data[1 + i], axes=[1, 2, 0]))\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:14:07.802307Z","iopub.execute_input":"2024-11-25T21:14:07.802659Z","iopub.status.idle":"2024-11-25T21:14:22.232216Z","shell.execute_reply.started":"2024-11-25T21:14:07.802623Z","shell.execute_reply":"2024-11-25T21:14:22.231356Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Кодируем метки из предоставленных метаданных","metadata":{}},{"cell_type":"code","source":"df_metadata = pd.read_csv(\"../input/birdclef-2021/train_metadata.csv\")\nlabel_encoder = LabelEncoder().fit(sorted(df_metadata[\"primary_label\"].unique()))\nnum_classes = len(label_encoder.classes_)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:15:46.790986Z","iopub.execute_input":"2024-11-25T21:15:46.791338Z","iopub.status.idle":"2024-11-25T21:15:47.021305Z","shell.execute_reply.started":"2024-11-25T21:15:46.791303Z","shell.execute_reply":"2024-11-25T21:15:47.020623Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Загрузка модели\n\nТак как веса лежат локально, то сначала необходимо создать объект класса с содержанием модели, и потом отдельно загрузить веса. При возможности используем GPU","metadata":{}},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(device)\n\ndef load_model(path: str):\n    model = resnest50(pretrained=False)\n    model.fc = torch.nn.Linear(model.fc.in_features, num_classes)\n    dummy_device = torch.device(\"cpu\")\n    d = torch.load(path, map_location=dummy_device)\n    for key in list(d.keys()):\n        d[key.replace(\"model.\", \"\")] = d.pop(key)\n    model.load_state_dict(d)\n    model = model.to(device)\n    model = model.eval()\n    return model\n    \nmodel = load_model(\"../input/kkiller-birdclef-models-public/birdclef_resnest50_fold0_epoch_10_f1_val_06471_20210417161101.pth\")\nmodel","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:15:47.022799Z","iopub.execute_input":"2024-11-25T21:15:47.023070Z","iopub.status.idle":"2024-11-25T21:15:49.841606Z","shell.execute_reply.started":"2024-11-25T21:15:47.023044Z","shell.execute_reply":"2024-11-25T21:15:49.840621Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Функции обработки выходных данных модели\n\n## Функция `apply_treshold`\n\nВыбирает результаты, в которых вероятность принадлежности той или иной птице выше порогового значения\n\n## Функция `convert_id_to_names`\n\nПреобразовывает при помощи `LabelEncoder` полученные id в название птиц\n\n## Функция `process_input`\n\nОбрабатывает батч входных данных:\n* Перемещает данные на устройство выполнения\n* Отправляет данные в модель\n* Применяет sigmoid функцию для \"нормализации\" данных\n* Отбирает данные по пороговому значению\n* Преобразовывает id в названия птиц","metadata":{}},{"cell_type":"code","source":"@torch.no_grad()\ndef apply_treshold(output):\n    thresh = 0.1\n    osotred = (-output).argsort(1)\n    npreds = (output > thresh).sum(1)\n    return [\n        o[:p].cpu().numpy().tolist()\n        for o, p in zip(osotred, npreds)\n    ]\n\n\ndef convert_id_to_names(preds):\n    return [\n        \" \".join(label_encoder.inverse_transform(p))\n        if p else \"nocall\" for p in preds \n    ]\n\n\ndef process_input(inp):\n    inp = torch.from_numpy(inp).to(device)\n    output = torch.sigmoid(model(inp))\n    return convert_id_to_names(apply_treshold(output))\n\n# test on some samples from the recordings\ninp = np.stack([test_data[240 + i] for i in range(10)])\npred = process_input(inp)\nfor i, res in enumerate(pred):\n    print(f'found {res} in {str(i * 5)}-{str(i * 5 + 5)} sec of audio')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:15:49.842827Z","iopub.execute_input":"2024-11-25T21:15:49.843543Z","iopub.status.idle":"2024-11-25T21:15:51.452640Z","shell.execute_reply.started":"2024-11-25T21:15:49.843502Z","shell.execute_reply":"2024-11-25T21:15:51.451754Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Функция `predict`\n\nОбрабатывает данные. Используется батч из 64 элементов","metadata":{}},{"cell_type":"code","source":"def predict(test_data):\n    batch_size = 64\n    ldata = len(test_data)\n    upper_bound = math.ceil(ldata / batch_size) * batch_size\n    preds = []\n    with torch.no_grad():\n        for idx in range(0, upper_bound, batch_size):\n            inp = np.stack([test_data[idx + i] for i in range(batch_size) if i + idx < ldata])\n            pred = process_input(inp)\n            preds.extend(pred)\n    return preds\n\npreds = predict(test_data)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:15:51.454593Z","iopub.execute_input":"2024-11-25T21:15:51.454874Z","iopub.status.idle":"2024-11-25T21:16:56.874866Z","shell.execute_reply.started":"2024-11-25T21:15:51.454846Z","shell.execute_reply":"2024-11-25T21:16:56.873653Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Функция `make_submission`\n\nФормирует из данных итоговый csv файл для отправки","metadata":{}},{"cell_type":"code","source":"def make_submission(origin_df, preds):\n    data = {\n        \"row_id\": [],\n        \"birds\": [],\n    }\n    \n    for row, pred in zip(origin_df[\"row_id\"], preds):\n        data[\"row_id\"].append(row)\n        data[\"birds\"].append(pred)\n    return pd.DataFrame(data)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:16:56.876416Z","iopub.execute_input":"2024-11-25T21:16:56.877388Z","iopub.status.idle":"2024-11-25T21:16:56.885467Z","shell.execute_reply.started":"2024-11-25T21:16:56.877332Z","shell.execute_reply":"2024-11-25T21:16:56.883682Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = make_submission(df, preds)\nprint(submission)\nsubmission.to_csv(\"submission.csv\", index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2024-11-25T21:16:56.887026Z","iopub.execute_input":"2024-11-25T21:16:56.887719Z","iopub.status.idle":"2024-11-25T21:16:56.911336Z","shell.execute_reply.started":"2024-11-25T21:16:56.887668Z","shell.execute_reply":"2024-11-25T21:16:56.910490Z"}},"outputs":[],"execution_count":null}]}