{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"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":"gpu","dataSources":[{"sourceId":21669,"databundleVersionId":1692278,"sourceType":"competition"}],"dockerImageVersionId":31236,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport warnings\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom tqdm import tqdm\nfrom sklearn.model_selection import train_test_split","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:47:59.418802Z","iopub.execute_input":"2025-12-18T18:47:59.419688Z","iopub.status.idle":"2025-12-18T18:47:59.575326Z","shell.execute_reply.started":"2025-12-18T18:47:59.419654Z","shell.execute_reply":"2025-12-18T18:47:59.574764Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"warnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:00.773282Z","iopub.execute_input":"2025-12-18T18:48:00.774314Z","iopub.status.idle":"2025-12-18T18:48:00.777629Z","shell.execute_reply.started":"2025-12-18T18:48:00.774282Z","shell.execute_reply":"2025-12-18T18:48:00.776851Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:11.682713Z","iopub.execute_input":"2025-12-18T18:48:11.683030Z","iopub.status.idle":"2025-12-18T18:48:11.687801Z","shell.execute_reply.started":"2025-12-18T18:48:11.683003Z","shell.execute_reply":"2025-12-18T18:48:11.687169Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/rfcx-species-audio-detection\"\nTRAIN_AUDIO_DIR = f\"{DATA_DIR}/train\"\nTEST_AUDIO_DIR  = f\"{DATA_DIR}/test\"\n\nSR = 48000\nDURATION = 60\nSAMPLES = SR * DURATION\n\nN_MELS = 128\nN_FFT = 2048\nHOP_LENGTH = 512\n\nNUM_CLASSES = 24\n\nWINDOW_SEC = 5\nWINDOW_SAMPLES = SR * WINDOW_SEC","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:19.024542Z","iopub.execute_input":"2025-12-18T18:48:19.024839Z","iopub.status.idle":"2025-12-18T18:48:19.029521Z","shell.execute_reply.started":"2025-12-18T18:48:19.024815Z","shell.execute_reply":"2025-12-18T18:48:19.028753Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_tp = pd.read_csv(f\"{DATA_DIR}/train_tp.csv\")\ntrain_tp.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:27.371713Z","iopub.execute_input":"2025-12-18T18:48:27.372312Z","iopub.status.idle":"2025-12-18T18:48:27.389419Z","shell.execute_reply.started":"2025-12-18T18:48:27.372278Z","shell.execute_reply":"2025-12-18T18:48:27.388683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"grouped = train_tp.groupby(\"recording_id\")[\"species_id\"].apply(list)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:29.573096Z","iopub.execute_input":"2025-12-18T18:48:29.573827Z","iopub.status.idle":"2025-12-18T18:48:29.599123Z","shell.execute_reply.started":"2025-12-18T18:48:29.573798Z","shell.execute_reply":"2025-12-18T18:48:29.598629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def build_target(species):\n    t = np.zeros(NUM_CLASSES, dtype=np.float32)\n    for s in species:\n        t[s] = 1.0\n    return t","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:37.260748Z","iopub.execute_input":"2025-12-18T18:48:37.261472Z","iopub.status.idle":"2025-12-18T18:48:37.265276Z","shell.execute_reply.started":"2025-12-18T18:48:37.261340Z","shell.execute_reply":"2025-12-18T18:48:37.264656Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"targets = (\n    train_tp\n    .groupby(\"recording_id\")[\"species_id\"]\n    .apply(list)\n    .apply(build_target)\n    .to_dict()\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:42.218097Z","iopub.execute_input":"2025-12-18T18:48:42.218795Z","iopub.status.idle":"2025-12-18T18:48:42.243085Z","shell.execute_reply.started":"2025-12-18T18:48:42.218768Z","shell.execute_reply":"2025-12-18T18:48:42.242538Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_audio(path):\n    audio, _ = librosa.load(path, sr=SR, mono=True)\n    if len(audio) < SAMPLES:\n        audio = np.pad(audio, (0, SAMPLES - len(audio)))\n    else:\n        audio = audio[:SAMPLES]\n    return audio\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:47.791050Z","iopub.execute_input":"2025-12-18T18:48:47.791435Z","iopub.status.idle":"2025-12-18T18:48:47.795624Z","shell.execute_reply.started":"2025-12-18T18:48:47.791408Z","shell.execute_reply":"2025-12-18T18:48:47.794970Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio_to_mel(audio):\n    mel = librosa.feature.melspectrogram(\n        y=audio,\n        sr=SR,\n        n_fft=N_FFT,\n        hop_length=HOP_LENGTH,\n        n_mels=N_MELS\n    )\n    mel = librosa.power_to_db(mel)\n    mel = (mel - mel.mean()) / (mel.std() + 1e-6)\n    return mel.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:52.872992Z","iopub.execute_input":"2025-12-18T18:48:52.873407Z","iopub.status.idle":"2025-12-18T18:48:52.877611Z","shell.execute_reply.started":"2025-12-18T18:48:52.873382Z","shell.execute_reply":"2025-12-18T18:48:52.876967Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RFCXWindowDataset(Dataset):\n    def __init__(self, audio_dir, targets):\n        self.audio_dir = audio_dir\n        self.items = []\n\n        for rec_id, target in targets.items():\n            audio = load_audio(os.path.join(audio_dir, f\"{rec_id}.flac\"))\n            n_windows = len(audio) // WINDOW_SAMPLES\n\n            for w in range(n_windows):\n                start = w * WINDOW_SAMPLES\n                self.items.append((rec_id, start, target))\n\n    def __len__(self):\n        return len(self.items)\n\n    def __getitem__(self, idx):\n        rec_id, start, target = self.items[idx]\n\n        audio, _ = librosa.load(\n            os.path.join(self.audio_dir, f\"{rec_id}.flac\"),\n            sr=SR,\n            mono=True,\n            offset=start / SR,\n            duration=WINDOW_SEC\n        )\n\n        if len(audio) < WINDOW_SAMPLES:\n            audio = np.pad(audio, (0, WINDOW_SAMPLES - len(audio)))\n\n        mel = audio_to_mel(audio)\n        mel = mel[None, :, :]  # (1, 128, T)\n\n        return rec_id, torch.tensor(mel), torch.tensor(target)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:48:57.931362Z","iopub.execute_input":"2025-12-18T18:48:57.932107Z","iopub.status.idle":"2025-12-18T18:48:57.938364Z","shell.execute_reply.started":"2025-12-18T18:48:57.932081Z","shell.execute_reply":"2025-12-18T18:48:57.937391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ids, valid_ids = train_test_split(\n    list(targets.keys()), test_size=0.2, random_state=42\n)\n\ntrain_targets = {k: targets[k] for k in train_ids}\nvalid_targets = {k: targets[k] for k in valid_ids}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:49:12.765855Z","iopub.execute_input":"2025-12-18T18:49:12.766494Z","iopub.status.idle":"2025-12-18T18:49:12.771640Z","shell.execute_reply.started":"2025-12-18T18:49:12.766466Z","shell.execute_reply":"2025-12-18T18:49:12.770839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(\n    RFCXWindowDataset(TRAIN_AUDIO_DIR, train_targets),\n    batch_size=16,\n    shuffle=True,\n    num_workers=2,\n    pin_memory=True\n)\n\nvalid_loader = DataLoader(\n    RFCXWindowDataset(TRAIN_AUDIO_DIR, valid_targets),\n    batch_size=16,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=True\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:49:18.533366Z","iopub.execute_input":"2025-12-18T18:49:18.533658Z","iopub.status.idle":"2025-12-18T18:50:24.736174Z","shell.execute_reply.started":"2025-12-18T18:49:18.533634Z","shell.execute_reply":"2025-12-18T18:50:24.735552Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SimpleCNN(nn.Module):\n    def __init__(self):\n        super().__init__()\n\n        self.net = nn.Sequential(\n            nn.Conv2d(1, 16, 3, padding=1),\n            nn.BatchNorm2d(16),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n\n            nn.Conv2d(16, 32, 3, padding=1),\n            nn.BatchNorm2d(32),\n            nn.ReLU(),\n            nn.MaxPool2d(2),\n\n            nn.Conv2d(32, 64, 3, padding=1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1)\n        )\n\n        self.fc = nn.Linear(64, NUM_CLASSES)\n\n    def forward(self, x):\n        x = self.net(x)\n        x = x.flatten(1)\n        return self.fc(x)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:50:24.744191Z","iopub.execute_input":"2025-12-18T18:50:24.744797Z","iopub.status.idle":"2025-12-18T18:50:24.757777Z","shell.execute_reply.started":"2025-12-18T18:50:24.744777Z","shell.execute_reply":"2025-12-18T18:50:24.757248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = SimpleCNN().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:50:56.367448Z","iopub.execute_input":"2025-12-18T18:50:56.368017Z","iopub.status.idle":"2025-12-18T18:50:56.376384Z","shell.execute_reply.started":"2025-12-18T18:50:56.367989Z","shell.execute_reply":"2025-12-18T18:50:56.375813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion = nn.BCEWithLogitsLoss()\noptimizer = torch.optim.Adam(model.parameters(), lr=1e-3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:50:59.314909Z","iopub.execute_input":"2025-12-18T18:50:59.315659Z","iopub.status.idle":"2025-12-18T18:50:59.320129Z","shell.execute_reply.started":"2025-12-18T18:50:59.315627Z","shell.execute_reply":"2025-12-18T18:50:59.319434Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def lwlrap(y_true, y_pred):\n    C = y_true.shape[1]\n    score = 0.0\n    weight = 0.0\n\n    for c in range(C):\n        if y_true[:, c].sum() == 0:\n            continue\n\n        order = np.argsort(-y_pred[:, c])\n        truth = y_true[order, c]\n\n        correct = 0\n        prec = 0.0\n        for i, t in enumerate(truth):\n            if t:\n                correct += 1\n                prec += correct / (i + 1)\n\n        score += prec\n        weight += y_true[:, c].sum()\n\n    return score / weight\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:51:00.490602Z","iopub.execute_input":"2025-12-18T18:51:00.491344Z","iopub.status.idle":"2025-12-18T18:51:00.496197Z","shell.execute_reply.started":"2025-12-18T18:51:00.491313Z","shell.execute_reply":"2025-12-18T18:51:00.495381Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader):\n    model.train()\n    loss_sum = 0\n\n    pbar = tqdm(loader, desc=\"Train\", leave=False)\n    for _, mel, target in pbar:\n        mel, target = mel.to(device), target.to(device)\n\n        optimizer.zero_grad()\n        loss = criterion(model(mel), target)\n        loss.backward()\n        optimizer.step()\n\n        loss_sum += loss.item()\n        pbar.set_postfix(loss=loss.item())\n\n    return loss_sum / len(loader)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:51:02.599479Z","iopub.execute_input":"2025-12-18T18:51:02.600352Z","iopub.status.idle":"2025-12-18T18:51:02.604971Z","shell.execute_reply.started":"2025-12-18T18:51:02.600322Z","shell.execute_reply":"2025-12-18T18:51:02.604206Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(model, loader):\n    model.eval()\n    preds, trues = {}, {}\n\n    with torch.no_grad():\n        for rec_id, mel, target in loader:\n            mel = mel.to(device)\n            prob = torch.sigmoid(model(mel)).cpu().numpy()\n\n            for i, r in enumerate(rec_id):\n                preds.setdefault(r, []).append(prob[i])\n                trues[r] = target[i].numpy()\n\n    y_true = []\n    y_pred = []\n\n    for r in preds:\n        y_true.append(trues[r])\n        y_pred.append(np.max(preds[r], axis=0))\n\n    return lwlrap(np.array(y_true), np.array(y_pred))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:51:04.553465Z","iopub.execute_input":"2025-12-18T18:51:04.553764Z","iopub.status.idle":"2025-12-18T18:51:04.559478Z","shell.execute_reply.started":"2025-12-18T18:51:04.553739Z","shell.execute_reply":"2025-12-18T18:51:04.558816Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_score = 0.0\nEPOCHS = 8\n\nfor epoch in range(EPOCHS):\n    print(f\"\\nEpoch {epoch+1}\")\n\n    train_loss = train_one_epoch(model, train_loader)\n    val_score = validate(model, valid_loader)\n\n    print(f\"loss={train_loss:.4f}, lwlrap={val_score:.4f}\")\n\n    if val_score > best_score:\n        best_score = val_score\n        torch.save(model.state_dict(), \"best_model.pth\")\n        print(\"Saved best model\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T18:52:02.175434Z","iopub.execute_input":"2025-12-18T18:52:02.175751Z","iopub.status.idle":"2025-12-18T19:00:09.675495Z","shell.execute_reply.started":"2025-12-18T18:52:02.175722Z","shell.execute_reply":"2025-12-18T19:00:09.674703Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(torch.load(\"best_model.pth\", map_location=device))\nmodel.eval()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T19:00:16.961521Z","iopub.execute_input":"2025-12-18T19:00:16.961844Z","iopub.status.idle":"2025-12-18T19:00:16.977596Z","shell.execute_reply.started":"2025-12-18T19:00:16.961814Z","shell.execute_reply":"2025-12-18T19:00:16.976990Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RFCXTestDataset(Dataset):\n    def __init__(self, audio_dir):\n        self.items = []\n        for f in os.listdir(audio_dir):\n            if f.endswith(\".flac\"):\n                rec_id = f.replace(\".flac\", \"\")\n                audio = load_audio(os.path.join(audio_dir, f))\n                for w in range(len(audio) // WINDOW_SAMPLES):\n                    self.items.append((rec_id, w * WINDOW_SAMPLES))\n\n    def __len__(self):\n        return len(self.items)\n\n    def __getitem__(self, idx):\n        rec_id, start = self.items[idx]\n        audio, _ = librosa.load(\n            os.path.join(TEST_AUDIO_DIR, f\"{rec_id}.flac\"),\n            sr=SR,\n            mono=True,\n            offset=start / SR,\n            duration=WINDOW_SEC\n        )\n        mel = audio_to_mel(audio)[None]\n        return rec_id, torch.tensor(mel)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T19:00:20.052697Z","iopub.execute_input":"2025-12-18T19:00:20.052981Z","iopub.status.idle":"2025-12-18T19:00:20.059012Z","shell.execute_reply.started":"2025-12-18T19:00:20.052958Z","shell.execute_reply":"2025-12-18T19:00:20.058210Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = DataLoader(\n    RFCXTestDataset(TEST_AUDIO_DIR),\n    batch_size=16,\n    shuffle=False\n)\n\npreds = {}\n\nwith torch.no_grad():\n    for rec_id, mel in tqdm(test_loader):\n        mel = mel.to(device)\n        prob = torch.sigmoid(model(mel)).cpu().numpy()\n\n        for i, r in enumerate(rec_id):\n            preds.setdefault(r, []).append(prob[i])\n\nsubmission = pd.DataFrame.from_dict(\n    {r: np.max(p, axis=0) for r, p in preds.items()},\n    orient=\"index\",\n    columns=[f\"s{i}\" for i in range(NUM_CLASSES)]\n)\n\nsubmission.index.name = \"recording_id\"\nsubmission.reset_index(inplace=True)\nsubmission.to_csv(\"submission.csv\", index=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-18T19:00:23.137207Z","iopub.execute_input":"2025-12-18T19:00:23.137530Z","iopub.status.idle":"2025-12-18T19:12:42.178606Z","shell.execute_reply.started":"2025-12-18T19:00:23.137502Z","shell.execute_reply":"2025-12-18T19:12:42.177747Z"}},"outputs":[],"execution_count":null}]}