{"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":21669,"databundleVersionId":1692278,"sourceType":"competition"}],"dockerImageVersionId":31154,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:38.920623Z","iopub.execute_input":"2025-11-02T14:11:38.920891Z","iopub.status.idle":"2025-11-02T14:11:38.927522Z","shell.execute_reply.started":"2025-11-02T14:11:38.920869Z","shell.execute_reply":"2025-11-02T14:11:38.926688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport librosa\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nimport timm\nfrom tqdm.auto import tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:38.928619Z","iopub.execute_input":"2025-11-02T14:11:38.929314Z","iopub.status.idle":"2025-11-02T14:11:50.529317Z","shell.execute_reply.started":"2025-11-02T14:11:38.929290Z","shell.execute_reply":"2025-11-02T14:11:50.528715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Настройки ---\nSEED = 42\ntorch.manual_seed(SEED)\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.530004Z","iopub.execute_input":"2025-11-02T14:11:50.530280Z","iopub.status.idle":"2025-11-02T14:11:50.621516Z","shell.execute_reply.started":"2025-11-02T14:11:50.530256Z","shell.execute_reply":"2025-11-02T14:11:50.620927Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_DIR = \"/kaggle/input/rfcx-species-audio-detection/train/\"\nCSV_PATH = \"/kaggle/input/rfcx-species-audio-detection/train_tp.csv\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.623427Z","iopub.execute_input":"2025-11-02T14:11:50.623662Z","iopub.status.idle":"2025-11-02T14:11:50.627322Z","shell.execute_reply.started":"2025-11-02T14:11:50.623643Z","shell.execute_reply":"2025-11-02T14:11:50.626442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Загрузка CSV ---\ndf = pd.read_csv(CSV_PATH)\ndf = df[['recording_id', 'species_id']].drop_duplicates()\n\n# --- Словари классов ---\nLABELS = sorted(df['species_id'].unique())\nLABEL2ID = {label: i for i, label in enumerate(LABELS)} \nID2LABEL = {i: label for label, i in LABEL2ID.items()}\n\n# train/valid split\ntrain_df, valid_df = train_test_split(df, test_size=0.2, random_state=SEED, stratify=df['species_id'])\n\nnum_classes = len(LABEL2ID)\nprint(f\"Найдено классов: {num_classes}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.628214Z","iopub.execute_input":"2025-11-02T14:11:50.628467Z","iopub.status.idle":"2025-11-02T14:11:50.674967Z","shell.execute_reply.started":"2025-11-02T14:11:50.628444Z","shell.execute_reply":"2025-11-02T14:11:50.674391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- SpecAugment + загрузка аудио ---\ndef spec_augment(mel, freq_mask_param=15, time_mask_param=25):\n    mel = mel.copy()\n    # freq mask\n    num_mel_channels = mel.shape[0]\n    f = np.random.randint(0, freq_mask_param)\n    f0 = np.random.randint(0, num_mel_channels - f)\n    mel[f0:f0+f, :] = 0\n    # time mask\n    num_time_steps = mel.shape[1]\n    t = np.random.randint(0, time_mask_param)\n    t0 = np.random.randint(0, num_time_steps - t)\n    mel[:, t0:t0+t] = 0\n    return mel\n\ndef load_audio(fp, sr=32000, n_mels=128, duration=5, augment=False):\n    y, _ = librosa.load(fp, sr=sr)\n    y = np.asfortranarray(y) \n    y = librosa.util.fix_length(y, size=sr * duration)\n    y = y / (np.max(np.abs(y)) + 1e-6)\n\n    # --- Аугментации ---\n    if augment:\n        if np.random.rand() < 0.5:\n            y = np.flip(y)\n        if np.random.rand() < 0.5:\n            n_steps = np.random.uniform(-2, 2)\n            y = librosa.effects.pitch_shift(y, sr=sr, n_steps=n_steps)\n        if np.random.rand() < 0.5:\n            rate = np.random.uniform(0.8, 1.2)\n            try:\n                y = librosa.effects.time_stretch(y=y, rate=rate)\n            except Exception as e:\n                print(f\"time_stretch error ({fp}): {e}\")\n        if np.random.rand() < 0.5:\n            noise = np.random.normal(0, 0.005, y.shape)\n            y = y + noise\n\n    # --- Преобразование в мел-спектрограмму ---\n    mel = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=n_mels)\n    mel_db = librosa.power_to_db(mel, ref=np.max)\n    mel_db = (mel_db - mel_db.min()) / (mel_db.max() - mel_db.min())\n\n    # --- Паддинг по времени ---\n    max_len = sr // 512 * duration\n    if mel_db.shape[1] < max_len:\n        mel_db = np.pad(mel_db, ((0, 0), (0, max_len - mel_db.shape[1])), mode='constant')\n    else:\n        mel_db = mel_db[:, :max_len]\n\n    return torch.tensor(mel_db).unsqueeze(0).float()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.675621Z","iopub.execute_input":"2025-11-02T14:11:50.675888Z","iopub.status.idle":"2025-11-02T14:11:50.685127Z","shell.execute_reply.started":"2025-11-02T14:11:50.675870Z","shell.execute_reply":"2025-11-02T14:11:50.684351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdFrogDataset(Dataset):\n    def __init__(self, df, data_dir, num_classes, augment=False):\n        self.df = df\n        self.data_dir = data_dir\n        self.augment = augment\n        self.num_classes = num_classes\n        self.labels = df.groupby(\"recording_id\")[\"species_id\"].apply(list).to_dict()\n\n    def __len__(self):\n        return len(self.labels)\n\n    def __getitem__(self, idx):\n        rec_id = list(self.labels.keys())[idx]\n        fp = os.path.join(self.data_dir, rec_id + \".flac\")\n        x = load_audio(fp, augment=self.augment)\n\n        y = torch.zeros(self.num_classes)\n        for sid in self.labels[rec_id]:\n            y[sid] = 1.0\n        return x, y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.686050Z","iopub.execute_input":"2025-11-02T14:11:50.686412Z","iopub.status.idle":"2025-11-02T14:11:50.701442Z","shell.execute_reply.started":"2025-11-02T14:11:50.686387Z","shell.execute_reply":"2025-11-02T14:11:50.700699Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_ds = BirdFrogDataset(train_df, DATA_DIR, num_classes, augment=True)\nvalid_ds = BirdFrogDataset(valid_df, DATA_DIR, num_classes, augment=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=0)\nvalid_loader = DataLoader(valid_ds, batch_size=16, shuffle=False, num_workers=0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.702274Z","iopub.execute_input":"2025-11-02T14:11:50.702559Z","iopub.status.idle":"2025-11-02T14:11:50.735377Z","shell.execute_reply.started":"2025-11-02T14:11:50.702537Z","shell.execute_reply":"2025-11-02T14:11:50.734660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Модель ---\nclass AudioClassifier(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        self.backbone = timm.create_model(\"efficientnet_b0\", pretrained=True, in_chans=1, num_classes=0)\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n        in_features = self.backbone.num_features\n        self.head = nn.Sequential(\n            nn.Linear(in_features, 512),\n            nn.BatchNorm1d(512),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        x = self.backbone(x)\n        return self.head(x)\n\nmodel = AudioClassifier(num_classes).to(DEVICE)\noptimizer = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-2)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)\ncriterion = nn.BCEWithLogitsLoss()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:50.736050Z","iopub.execute_input":"2025-11-02T14:11:50.736258Z","iopub.status.idle":"2025-11-02T14:11:52.005815Z","shell.execute_reply.started":"2025-11-02T14:11:50.736243Z","shell.execute_reply":"2025-11-02T14:11:52.005146Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def accuracy_multi(preds, y, threshold=0.5):\n    preds = torch.sigmoid(preds)\n    preds = (preds > threshold).float()\n    correct = (preds == y).float().mean().item()\n    return correct","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:52.007766Z","iopub.execute_input":"2025-11-02T14:11:52.007999Z","iopub.status.idle":"2025-11-02T14:11:52.011868Z","shell.execute_reply.started":"2025-11-02T14:11:52.007983Z","shell.execute_reply":"2025-11-02T14:11:52.011228Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Train / Eval ---\ndef train_epoch(model, loader):\n    model.train()\n    total_loss = 0\n    correct, count = 0, 0\n    for x, y in tqdm(loader):\n        x, y = x.to(DEVICE), y.to(DEVICE)\n        optimizer.zero_grad()\n        preds = model(x)\n        loss = criterion(preds, y)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item() * len(y)\n        correct += accuracy_multi(preds, y)\n        count += 1\n    return total_loss / count, correct / count\n\ndef eval_epoch(model, loader):\n    model.eval()\n    total_loss = 0\n    correct, count = 0, 0\n    with torch.no_grad():\n        for x, y in tqdm(loader):\n            x, y = x.to(DEVICE), y.to(DEVICE)\n            preds = model(x)\n            loss = criterion(preds, y)\n            total_loss += loss.item() * len(y)\n            correct += accuracy_multi(preds, y)\n            count += 1\n    return total_loss / count, correct / count","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:11:52.012447Z","iopub.execute_input":"2025-11-02T14:11:52.012745Z","iopub.status.idle":"2025-11-02T14:11:52.027387Z","shell.execute_reply.started":"2025-11-02T14:11:52.012724Z","shell.execute_reply":"2025-11-02T14:11:52.026793Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Обучение с ранней остановкой ---\nbest_val = 0\npatience, wait = 3, 0\nnum_epochs = 3\n\nfor epoch in range(num_epochs):\n    print(f\"\\n===== Epoch {epoch+1}/{num_epochs} =====\")\n\n    if epoch == 1:\n        for param in model.backbone.parameters():\n            param.requires_grad = True\n\n    train_loss, train_acc = train_epoch(model, train_loader)\n    val_loss, val_acc = eval_epoch(model, valid_loader)\n    scheduler.step()\n\n    print(f\"Epoch {epoch+1}: train_acc={train_acc:.3f}, val_acc={val_acc:.3f}, \"\n          f\"train_loss={train_loss:.4f}, val_loss={val_loss:.4f}\")\n\n    if val_acc > best_val:\n        best_val = val_acc\n        torch.save(model.state_dict(), \"best_model_singlelabel.pth\")\n        print(f\"New best val_acc={best_val:.3f} — модель сохранена\")\n        wait = 0\n    else:\n        wait += 1\n        if wait >= patience:\n            print(\"Early stopping triggered\")\n            break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-02T14:21:47.391323Z","iopub.execute_input":"2025-11-02T14:21:47.391932Z","iopub.status.idle":"2025-11-02T14:31:36.039008Z","shell.execute_reply.started":"2025-11-02T14:21:47.391912Z","shell.execute_reply":"2025-11-02T14:31:36.038119Z"}},"outputs":[],"execution_count":null}]}