{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":21669,"databundleVersionId":1692278,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"d772af84","cell_type":"code","source":"# --- core\nimport os, csv, copy, random, warnings\nimport numpy as np\nimport pandas as pd\n\n# --- audio / images\nimport librosa\nfrom skimage.filters import gaussian\nfrom skimage.transform import resize\nfrom skimage import exposure, util\n\n# --- torch / training\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.models import resnet50\n\n# --- utils\nfrom concurrent.futures import ThreadPoolExecutor\nfrom sklearn.model_selection import KFold\nfrom tqdm import tqdm\n\nwarnings.filterwarnings(\"ignore\")\n\ndevice = 'cuda:0' if torch.cuda.is_available() else 'cpu'\nprint(device)\n\n\nLABELS = 24\nSR = 48000\nLENGTH = 10 * SR\nF_MIN = 24000\nF_MAX = 0\nLEARNING_RATE = 2e-4\nEPOCHS = 20\nN_FOLD = 5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:23.518731Z","iopub.execute_input":"2025-12-24T02:52:23.519025Z","iopub.status.idle":"2025-12-24T02:52:32.016412Z","shell.execute_reply.started":"2025-12-24T02:52:23.518999Z","shell.execute_reply":"2025-12-24T02:52:32.015724Z"}},"outputs":[],"execution_count":null},{"id":"6abd0a6b-dcf6-4aed-964f-b53dc593694a","cell_type":"markdown","source":"### Подготовка спектрограмм","metadata":{}},{"id":"ac1f53ef","cell_type":"code","source":"class SpectrogramAug:\n    def __init__(self):\n        self._ops = (\n            self._noise,\n            self._contrast,\n            self._flip_lr,\n            self._flip_ud,\n        )\n\n    @staticmethod\n    def _flip_lr(img2d):\n        return np.stack((img2d[:, ::-1],) * 3)\n\n    @staticmethod\n    def _flip_ud(img2d):\n        return np.stack((img2d[::-1, :],) * 3)\n\n    @staticmethod\n    def _noise(img2d):\n        noisy = util.random_noise(img2d)\n        return np.stack((noisy,) * 3)\n\n    @staticmethod\n    def _contrast(img2d):\n        stretched = exposure.rescale_intensity(img2d)\n        return np.stack((stretched,) * 3)\n\n    def __call__(self, img2d):\n        op = random.choice(self._ops)\n        return op(img2d)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:32.017702Z","iopub.execute_input":"2025-12-24T02:52:32.018044Z","iopub.status.idle":"2025-12-24T02:52:32.024301Z","shell.execute_reply.started":"2025-12-24T02:52:32.018021Z","shell.execute_reply":"2025-12-24T02:52:32.023356Z"}},"outputs":[],"execution_count":null},{"id":"f0928ff6","cell_type":"code","source":"def mel_to_uint8(mel_db):\n    \"\"\"Приводит 2D mel-спектр (dB) к uint8-картинке 0..255.\"\"\"\n    mel_db = resize(mel_db, (224, 400))\n    eps = 1e-6\n\n    mu = mel_db.mean()\n    sigma = mel_db.std()\n\n    z = (mel_db - mu) / (sigma + eps)\n    lo, hi = z.min(), z.max()\n    scaled = 255.0 * (z - lo) / (hi - lo)\n    return np.asarray(scaled.astype(np.uint8))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:32.025476Z","iopub.execute_input":"2025-12-24T02:52:32.025743Z","iopub.status.idle":"2025-12-24T02:52:32.043468Z","shell.execute_reply.started":"2025-12-24T02:52:32.025712Z","shell.execute_reply":"2025-12-24T02:52:32.042780Z"}},"outputs":[],"execution_count":null},{"id":"53bf2098","cell_type":"code","source":"def build_net():\n    net = resnet50(pretrained=True)\n    in_features = net.fc.in_features\n    net.fc = nn.Linear(in_features, LABELS)\n    return net.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:32.045008Z","iopub.execute_input":"2025-12-24T02:52:32.045401Z","iopub.status.idle":"2025-12-24T02:52:32.059682Z","shell.execute_reply.started":"2025-12-24T02:52:32.045380Z","shell.execute_reply":"2025-12-24T02:52:32.059007Z"}},"outputs":[],"execution_count":null},{"id":"9773875b","cell_type":"code","source":"meta = pd.read_csv(\"/kaggle/input/rfcx-species-audio-detection/train_tp.csv\")\n\n# берём минимальный f_min и максимальный f_max, затем расширяем 0.9/1.1\nF_MIN = float(meta[\"f_min\"].min())\nF_MAX = float(meta[\"f_max\"].max())\n\nF_MIN = int(F_MIN * 0.9)\nF_MAX = int(F_MAX * 1.1)\n\nrec_ids = np.array(meta[\"recording_id\"].tolist())\nsp_ids  = np.array(meta[\"species_id\"].tolist())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:32.060421Z","iopub.execute_input":"2025-12-24T02:52:32.060595Z","iopub.status.idle":"2025-12-24T02:52:32.103710Z","shell.execute_reply.started":"2025-12-24T02:52:32.060578Z","shell.execute_reply":"2025-12-24T02:52:32.102967Z"}},"outputs":[],"execution_count":null},{"id":"ba13c66a","cell_type":"code","source":"_cache = {}\n\ndef _clip_center_window(wav, sr, t0, t1, win_len=LENGTH):\n    a = int(t0 * sr)\n    b = int(t1 * sr)\n\n    mid = np.round((a + b) / 2)\n    left = max(mid - win_len // 2, 0)\n    right = min(left + win_len, len(wav))\n\n    left = right - win_len if (right - left) < win_len else left\n    return wav[int(left):int(right)]\n\ndef _one_train_item(i):\n    rid = rec_ids[i]\n    sid = sp_ids[i]\n\n    wav, sr = librosa.load(f\"/kaggle/input/rfcx-species-audio-detection/train/{rid}.flac\", sr=None)\n    seg = _clip_center_window(\n        wav, sr,\n        meta.at[i, \"t_min\"],\n        meta.at[i, \"t_max\"],\n        win_len=LENGTH,\n    )\n\n    mel = librosa.feature.melspectrogram(y=seg, sr=sr, fmin=F_MIN, fmax=F_MAX)\n    mel_db = librosa.power_to_db(mel, top_db=80)\n\n    img = mel_to_uint8(mel_db)\n    return rid, img\n\n# параллельная подготовка кэша изображений\nwith ThreadPoolExecutor() as pool:\n    for rid, img in pool.map(_one_train_item, range(len(meta))):\n        _cache[rid] = img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:52:32.104638Z","iopub.execute_input":"2025-12-24T02:52:32.104866Z","iopub.status.idle":"2025-12-24T02:53:56.455194Z","shell.execute_reply.started":"2025-12-24T02:52:32.104849Z","shell.execute_reply":"2025-12-24T02:53:56.454556Z"}},"outputs":[],"execution_count":null},{"id":"ee8fa289","cell_type":"code","source":"class RFCXSpectroDataset(Dataset):\n    def __init__(self, ids, targets, mode=\"train\", aug=None, cache=None):\n        self.ids = np.asarray(ids)\n        self.targets = np.asarray(targets)\n        self.mode = mode\n        self.aug = aug\n        self.cache = _cache if cache is None else cache\n\n    def __len__(self):\n        return self.ids.shape[0]\n\n    def __getitem__(self, idx):\n        rid = self.ids[idx]\n        y = int(self.targets[idx])\n        img2d = self.cache[rid]\n\n        if self.mode == \"train\" and self.aug is not None:\n            x = self.aug(img2d)\n        else:\n            x = np.stack((img2d, img2d, img2d))\n\n        return x, y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:53:56.455966Z","iopub.execute_input":"2025-12-24T02:53:56.457037Z","iopub.status.idle":"2025-12-24T02:53:56.467156Z","shell.execute_reply.started":"2025-12-24T02:53:56.457006Z","shell.execute_reply":"2025-12-24T02:53:56.466599Z"}},"outputs":[],"execution_count":null},{"id":"e27bfb6b-1718-4985-b723-9ff75c28944d","cell_type":"markdown","source":"### Подготовка модели","metadata":{}},{"id":"b5cc81cf","cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\naugment = SpectrogramAug()\n\ndef fit_one_model(net, crit, dl_train, dl_valid, optim, sched):\n    best_w = copy.deepcopy(net.state_dict())\n    best_acc = 0.0\n    tr_hist, va_hist = [], []\n\n    for ep in tqdm(range(1, EPOCHS + 1)):\n        # ------------------ TRAIN ------------------\n        net.train()\n        batch_losses = []\n\n        for _, batch in enumerate(dl_train):\n            x, y = batch\n            x = x.float().to(device)\n            y = y.to(device)\n\n            optim.zero_grad()\n            out = net(x)\n            loss = crit(out, y)\n            loss.backward()\n            optim.step()\n\n            batch_losses.append(loss.item())\n\n        tr_hist.append(batch_losses)\n\n        # ------------------ VALIDATION ------------------\n        net.eval()\n        val_losses, trace_y, trace_yhat = [], [], []\n\n        with torch.no_grad():\n            for _, batch in enumerate(dl_valid):\n                x, y = batch\n                x = x.float().to(device)\n                y = y.to(device)\n\n                out = net(x)\n                loss = crit(out, y)\n                val_losses.append(loss.item())\n\n                # Сохраняем батчи для accuracy\n                trace_y.append(y.cpu().numpy())\n                trace_yhat.append(out.cpu().numpy())\n\n        va_hist.append(val_losses)\n\n        # ------------------ METRICS ------------------\n        if len(trace_y) == 0:\n            print(\"Warning: empty fold, skipping metric calculation\")\n            acc = 0\n        else:\n            trace_y = np.concatenate(trace_y)\n            trace_yhat = np.concatenate(trace_yhat)\n            acc = np.mean(trace_yhat.argmax(axis=1) == trace_y)\n\n            print(\"epoch = %d, train_loss = %.5f, val_loss = %.5f, val_accuracy = %.5f\" % (\n                ep, np.mean(tr_hist[-1]), np.mean(va_hist[-1]), acc))\n\n        # ------------------ SCHEDULER ------------------\n        if len(va_hist[-1]) > 0:\n            sched.step(np.mean(va_hist[-1]))\n\n        # ------------------ BEST MODEL ------------------\n        if acc > best_acc:\n            best_acc = acc\n            best_w = copy.deepcopy(net.state_dict())\n\n    net.load_state_dict(best_w)\n    return net\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:53:56.469198Z","iopub.execute_input":"2025-12-24T02:53:56.469438Z","iopub.status.idle":"2025-12-24T02:53:56.496606Z","shell.execute_reply.started":"2025-12-24T02:53:56.469417Z","shell.execute_reply":"2025-12-24T02:53:56.495955Z"}},"outputs":[],"execution_count":null},{"id":"15a75c9c-e84f-4db4-9d51-65fea71c81b2","cell_type":"code","source":"print(\"Total samples:\", len(rec_ids))\nprint(\"Unique species:\", len(np.unique(sp_ids)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:53:56.498480Z","iopub.execute_input":"2025-12-24T02:53:56.499355Z","iopub.status.idle":"2025-12-24T02:53:56.522899Z","shell.execute_reply.started":"2025-12-24T02:53:56.499331Z","shell.execute_reply":"2025-12-24T02:53:56.522439Z"}},"outputs":[],"execution_count":null},{"id":"67c91f9d-0f70-4ea6-9d04-0b4c997a9a06","cell_type":"markdown","source":"### Обучение модели","metadata":{}},{"id":"92cefa6a","cell_type":"code","source":"from sklearn.model_selection import StratifiedKFold\nprint(f\"Cache size: {len(_cache)}\")\nprint(f\"Some keys: {list(_cache.keys())[:5]}\")\nmissing_keys = [rid for rid in rec_ids if rid not in _cache]\nprint(f\"Missing keys in cache: {missing_keys[:10]}\")\n\nkf = StratifiedKFold(n_splits=N_FOLD, shuffle=True, random_state=563)\nfor fold_idx, (tr_idx, va_idx) in enumerate(kf.split(rec_ids, sp_ids)):\n    print(\"Fold\", fold_idx)\n\n    tr_ids = np.take(rec_ids, tr_idx)\n    tr_y   = np.take(sp_ids, tr_idx, axis=0)\n    va_ids = np.take(rec_ids, va_idx)\n    va_y   = np.take(sp_ids, va_idx, axis=0)\n\n    # Проверка размеров фолдов\n    print(f\"Fold {fold_idx}: train size = {len(tr_idx)}, valid size = {len(va_idx)}\")\n    print(f\"Example train IDs: {rec_ids[tr_idx[:5]]}\")\n    print(f\"Example valid IDs: {rec_ids[va_idx[:5]]}\")\n    \n\n    \n    ds_tr = RFCXSpectroDataset(tr_ids, tr_y, mode=\"train\", aug=augment)\n    ds_va = RFCXSpectroDataset(va_ids, va_y, mode=\"valid\", aug=None)\n     \n    batch_size_tr = min(8, len(ds_tr))\n    batch_size_va = min(8, len(ds_va))\n    \n    dl_tr = DataLoader(ds_tr, batch_size=batch_size_tr, shuffle=True, drop_last=False)\n    dl_va = DataLoader(ds_va, batch_size=batch_size_va, shuffle=False, drop_last=False)\n\n    \n    batch_size_tr = min(8, len(ds_tr))\n    batch_size_va = min(8, len(ds_va))\n    \n    dl_tr = DataLoader(ds_tr, batch_size=batch_size_tr, shuffle=True, drop_last=False)\n    dl_va = DataLoader(ds_va, batch_size=batch_size_va, shuffle=False, drop_last=False)\n\n    for x, y in dl_tr:\n        print(f\"Train batch: x shape = {x.shape}, y shape = {y.shape}\")\n        break\n    for x, y in dl_va:\n        print(f\"Valid batch: x shape = {x.shape}, y shape = {y.shape}\")\n        break\n    \n    net = build_net()\n    optim = torch.optim.Adam(net.parameters(), lr=LEARNING_RATE)\n    sched = torch.optim.lr_scheduler.ReduceLROnPlateau(optim, \"min\", patience=3)\n\n    net = fit_one_model(net, criterion, dl_tr, dl_va, optim, sched)\n    torch.save(net.state_dict(), f\"./model{fold_idx}.pt\")\n\n    del ds_tr, ds_va, dl_tr, dl_va, net, tr_ids, va_ids, tr_y, va_y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T02:53:56.524524Z","iopub.execute_input":"2025-12-24T02:53:56.524730Z","iopub.status.idle":"2025-12-24T03:32:54.409717Z","shell.execute_reply.started":"2025-12-24T02:53:56.524710Z","shell.execute_reply":"2025-12-24T03:32:54.408977Z"}},"outputs":[],"execution_count":null},{"id":"21dc20cc","cell_type":"code","source":"def load_test_file(f):\n    wav, sr = librosa.load('/kaggle/input/rfcx-species-audio-detection/test/' + f, sr=None)\n\n    segments = len(wav) / LENGTH\n    segments = int(np.ceil(segments))\n\n    mel_array = []\n\n    for i in range(0, segments):\n        if (i + 1) * LENGTH > len(wav):\n            slice = wav[len(wav) - LENGTH:len(wav)]\n        else:\n            slice = wav[i * LENGTH:(i + 1) * LENGTH]\n\n        spec = librosa.feature.melspectrogram(y=slice, sr=sr, fmin=F_MIN, fmax=F_MAX)\n        spec_db = librosa.power_to_db(spec, top_db=80)\n\n        img = mel_to_uint8(spec_db)\n        mel_spec = np.stack((img, img, img))\n        mel_array.append(mel_spec)\n\n    return mel_array","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T03:41:06.627632Z","iopub.execute_input":"2025-12-24T03:41:06.627913Z","iopub.status.idle":"2025-12-24T03:41:06.633487Z","shell.execute_reply.started":"2025-12-24T03:41:06.627891Z","shell.execute_reply":"2025-12-24T03:41:06.632788Z"}},"outputs":[],"execution_count":null},{"id":"f7af8884","cell_type":"code","source":"members = []\n\nfor i in range(N_FOLD):\n    model = build_net()\n\n    model.load_state_dict(torch.load('./model' + str(i) + '.pt'))\n    model.eval()\n\n    members.append(model)\n\n    os.remove('./model' + str(i) + '.pt')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T03:41:09.063200Z","iopub.execute_input":"2025-12-24T03:41:09.064038Z","iopub.status.idle":"2025-12-24T03:41:09.475740Z","shell.execute_reply.started":"2025-12-24T03:41:09.064005Z","shell.execute_reply":"2025-12-24T03:41:09.474842Z"}},"outputs":[],"execution_count":null},{"id":"42d87d67","cell_type":"code","source":"def load_and_predict(test_file, members):\n    data = load_test_file(test_file)\n    data = torch.tensor(data).float()\n\n    if torch.cuda.is_available():\n        data = data.cuda()\n\n    output_list = []\n    for m in members:\n        output = m(data)\n        maxed_output = torch.max(output, dim=0)[0]\n        maxed_output = maxed_output.cpu().detach()\n        output_list.append(maxed_output)\n\n    avg_maxed_output = torch.mean(torch.stack(output_list), dim=0)\n    file_id = test_file.split('.')[0]\n    return [file_id] + [out.item() for out in avg_maxed_output]\n\ndef save_submission(predictions, output_file='submission.csv'):\n    with open(output_file, 'w', newline='') as csvfile:\n        submission_writer = csv.writer(csvfile, delimiter=',')\n        submission_writer.writerow(['recording_id', 's0', 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', \n                                    's11', 's12', 's13', 's14', 's15', 's16', 's17', 's18', 's19', 's20', 's21', 's22', 's23'])\n        for pred in predictions:\n            submission_writer.writerow(pred)\n\ndef generate_predictions(test_files, members):\n    predictions = []\n\n    with ThreadPoolExecutor(max_workers=4) as executor:\n        futures = [executor.submit(load_and_predict, test_file, members) for test_file in test_files]\n        for future in futures:\n            predictions.append(future.result())\n\n    save_submission(predictions)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T03:41:12.700471Z","iopub.execute_input":"2025-12-24T03:41:12.700709Z","iopub.status.idle":"2025-12-24T03:41:12.707987Z","shell.execute_reply.started":"2025-12-24T03:41:12.700688Z","shell.execute_reply":"2025-12-24T03:41:12.707287Z"}},"outputs":[],"execution_count":null},{"id":"1906deea","cell_type":"code","source":"test_files = os.listdir('/kaggle/input/rfcx-species-audio-detection/test/')\n\nif torch.cuda.is_available():\n    members = [m.cuda() for m in members]\n\ngenerate_predictions(test_files, members)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T03:41:14.914571Z","iopub.execute_input":"2025-12-24T03:41:14.915133Z","iopub.status.idle":"2025-12-24T03:56:30.345146Z","shell.execute_reply.started":"2025-12-24T03:41:14.915107Z","shell.execute_reply":"2025-12-24T03:56:30.344151Z"}},"outputs":[],"execution_count":null}]}