{"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":8900,"databundleVersionId":862232,"sourceType":"competition"}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Лабораторная работа 1: Классификация произвольных звуков (FSDKaggle2018)","metadata":{}},{"cell_type":"markdown","source":"* Соревнование: Freesound General-Purpose Audio Tagging Challenge\n* Цель: построить пайплайн audio → log-mel → CNN → mAP@3 → submission.csv (top-3 метки).\n* Метрика: mAP@3 (в Kaggle можно предсказать до 3 меток на файл).","metadata":{}},{"cell_type":"markdown","source":"## Шаг 1. Пути к данным","metadata":{}},{"cell_type":"code","source":"from pathlib import Path\nDATA = Path(\"/kaggle/input/freesound-audio-tagging\")\n\nTRAIN_DIR = DATA / \"audio_train\"\nTEST_DIR  = DATA / \"audio_test\"\nSAMPLE_SUB = DATA / \"sample_submission.csv\"\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:46.053732Z","iopub.execute_input":"2025-12-26T22:51:46.054368Z","iopub.status.idle":"2025-12-26T22:51:46.058287Z","shell.execute_reply.started":"2025-12-26T22:51:46.054344Z","shell.execute_reply":"2025-12-26T22:51:46.057573Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 2. Импорты, конфиг и воспроизводимость","metadata":{}},{"cell_type":"code","source":"from dataclasses import dataclass\nimport random\nimport numpy as np\nimport torch\n\n@dataclass\nclass CFG:\n    seed: int = 42\n\n    # audio\n    sr: int = 32000\n    duration: float = 4.0\n    n_mels: int = 128\n    n_fft: int = 1024\n    hop_length: int = 320\n    fmin: int = 20\n    fmax: int = 16000\n\n    # train\n    train_bs: int = 32\n    valid_bs: int = 64\n    epochs: int = 10\n    lr: float = 3e-4\n    wd: float = 1e-2\n    num_workers: int = 2\n\n    # aug\n    specaug_p: float = 0.4\n\n    # noisy handling\n    noisy_weight: float = 0.5\n    noisy_label_smooth: float = 0.10\n\n    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\ndef set_seed(seed: int):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nset_seed(CFG.seed)\nprint(\"torch:\", torch.__version__, \"| device:\", CFG.device)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:46.059205Z","iopub.execute_input":"2025-12-26T22:51:46.059408Z","iopub.status.idle":"2025-12-26T22:51:46.078483Z","shell.execute_reply.started":"2025-12-26T22:51:46.059387Z","shell.execute_reply":"2025-12-26T22:51:46.077994Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 3. Загрузка разметки и “правильный” split","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport os\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\n\ndef read_meta(DATA: Path):\n    curated = DATA / \"train_curated.csv\"\n    noisy   = DATA / \"train_noisy.csv\"\n    train   = DATA / \"train.csv\"\n\n    if curated.exists() and noisy.exists():\n        df_c = pd.read_csv(curated); df_c[\"is_noisy\"] = 0\n        df_n = pd.read_csv(noisy);   df_n[\"is_noisy\"] = 1\n        df = pd.concat([df_c, df_n], ignore_index=True)\n        mode = \"curated_noisy\"\n    else:\n        df = pd.read_csv(train)\n        if \"manually_verified\" in df.columns:\n            df[\"is_noisy\"] = (df[\"manually_verified\"] == 0).astype(int)\n        else:\n            df[\"is_noisy\"] = 0\n        mode = \"train_csv\"\n    return df, mode\n\ndf, split_mode = read_meta(DATA)\n\ndf[\"filepath\"] = df[\"fname\"].apply(lambda x: str(TRAIN_DIR / x))\ndf = df[df[\"filepath\"].map(os.path.exists)].reset_index(drop=True)\n\nle = LabelEncoder()\ndf[\"label_idx\"] = le.fit_transform(df[\"label\"])\nCLASSES = le.classes_.tolist()\nnum_classes = len(CLASSES)\n\n# val — только clean\nclean_df = df[df[\"is_noisy\"] == 0].copy()\ntrain_clean, valid_df = train_test_split(\n    clean_df, test_size=0.1, random_state=CFG.seed, stratify=clean_df[\"label_idx\"]\n)\n\n# train — clean_train + noisy\ntrain_df = pd.concat([train_clean, df[df[\"is_noisy\"] == 1]], ignore_index=True)\n\nprint(\"mode:\", split_mode)\nprint(\"classes:\", num_classes)\nprint(\"train:\", len(train_df), \"| val:\", len(valid_df), \"| noisy_share_train:\", train_df[\"is_noisy\"].mean())\nprint(\"noisy_share_val:\", valid_df[\"is_noisy\"].mean())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:46.079644Z","iopub.execute_input":"2025-12-26T22:51:46.079869Z","iopub.status.idle":"2025-12-26T22:51:50.259636Z","shell.execute_reply.started":"2025-12-26T22:51:46.079850Z","shell.execute_reply":"2025-12-26T22:51:50.258995Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 4. Препроцессинг аудио: mono + ресемпл + фикс длина + log-mel + SpecAugment","metadata":{}},{"cell_type":"code","source":"import soundfile as sf\nimport librosa\n\nTARGET_SAMPLES = int(CFG.sr * CFG.duration)\n\ndef load_audio_mono(path: str, target_sr=CFG.sr) -> np.ndarray:\n    wav, sr = sf.read(path, always_2d=False)\n    wav = wav.astype(np.float32)\n    if wav.ndim == 2:\n        wav = wav.mean(axis=1)\n    if sr != target_sr:\n        wav = librosa.resample(wav, orig_sr=sr, target_sr=target_sr)\n    return wav\n\ndef fix_length(wav: np.ndarray, target=TARGET_SAMPLES, train=True) -> np.ndarray:\n    if len(wav) < target:\n        wav = np.pad(wav, (0, target - len(wav)), mode=\"constant\")\n    elif len(wav) > target:\n        start = np.random.randint(0, len(wav) - target + 1) if train else (len(wav) - target) // 2\n        wav = wav[start:start + target]\n    return wav\n\ndef wav_to_logmel(wav: np.ndarray) -> np.ndarray:\n    m = librosa.feature.melspectrogram(\n        y=wav, sr=CFG.sr,\n        n_fft=CFG.n_fft, hop_length=CFG.hop_length,\n        n_mels=CFG.n_mels, fmin=CFG.fmin, fmax=CFG.fmax, power=2.0\n    )\n    logm = librosa.power_to_db(m, ref=np.max)\n    logm = (logm - logm.mean()) / (logm.std() + 1e-6)  # per-sample z-score\n    return logm.astype(np.float32)\n\ndef spec_augment(spec: np.ndarray, max_mask_pct=0.1, num_masks=2) -> np.ndarray:\n    spec = spec.copy()\n    n_mels, n_steps = spec.shape\n    for _ in range(num_masks):\n        if np.random.rand() < 0.5:\n            f = max(1, int(max_mask_pct * n_mels))\n            f0 = np.random.randint(0, max(1, n_mels - f))\n            spec[f0:f0+f, :] = 0\n        else:\n            t = max(1, int(max_mask_pct * n_steps))\n            t0 = np.random.randint(0, max(1, n_steps - t))\n            spec[:, t0:t0+t] = 0\n    return spec\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:50.260809Z","iopub.execute_input":"2025-12-26T22:51:50.261058Z","iopub.status.idle":"2025-12-26T22:51:50.270344Z","shell.execute_reply.started":"2025-12-26T22:51:50.261038Z","shell.execute_reply":"2025-12-26T22:51:50.269741Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 5. Dataset и DataLoader (возвращаем is_noisy и sample_weight)","metadata":{}},{"cell_type":"code","source":"from torch.utils.data import Dataset, DataLoader\n\nclass FSDKDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, train=True):\n        self.df = df.reset_index(drop=True)\n        self.train = train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, i):\n        row = self.df.iloc[i]\n        wav = load_audio_mono(row.filepath)\n        wav = fix_length(wav, TARGET_SAMPLES, train=self.train)\n        spec = wav_to_logmel(wav)\n\n        if self.train and np.random.rand() < CFG.specaug_p:\n            spec = spec_augment(spec)\n\n        x = torch.from_numpy(spec).unsqueeze(0)  # [1, n_mels, T]\n        y = torch.tensor(row.label_idx).long()\n\n        is_noisy = int(row.is_noisy)\n        w = CFG.noisy_weight if is_noisy else 1.0\n\n        return x, y, torch.tensor(w).float(), torch.tensor(is_noisy).long()\n\ntrain_loader = DataLoader(FSDKDataset(train_df, True), batch_size=CFG.train_bs, shuffle=True,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=True)\nvalid_loader = DataLoader(FSDKDataset(valid_df, False), batch_size=CFG.valid_bs, shuffle=False,\n                          num_workers=CFG.num_workers, pin_memory=True)\n\nprint(\"batches:\", len(train_loader), len(valid_loader))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:50.271012Z","iopub.execute_input":"2025-12-26T22:51:50.271651Z","iopub.status.idle":"2025-12-26T22:51:50.295155Z","shell.execute_reply.started":"2025-12-26T22:51:50.271625Z","shell.execute_reply":"2025-12-26T22:51:50.294482Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 6. Модель (CNN на спектрограммах)","metadata":{}},{"cell_type":"code","source":"import torch.nn as nn\n\nclass ConvBlock(nn.Module):\n    def __init__(self, in_ch, out_ch):\n        super().__init__()\n        self.block = nn.Sequential(\n            nn.Conv2d(in_ch, out_ch, 3, padding=1),\n            nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),\n            nn.Conv2d(out_ch, out_ch, 3, padding=1),\n            nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True),\n            nn.MaxPool2d(2)\n        )\n    def forward(self, x): \n        return self.block(x)\n\nclass CnnSpec(nn.Module):\n    def __init__(self, n_classes):\n        super().__init__()\n        self.features = nn.Sequential(\n            ConvBlock(1, 32), ConvBlock(32, 64),\n            ConvBlock(64, 128), ConvBlock(128, 256)\n        )\n        self.pool = nn.AdaptiveAvgPool2d(1)\n        self.drop = nn.Dropout(0.3)\n        self.fc = nn.Linear(256, n_classes)\n\n    def forward(self, x):\n        x = self.features(x)\n        x = self.pool(x).flatten(1)\n        x = self.drop(x)\n        return self.fc(x)\n\nmodel = CnnSpec(num_classes).to(CFG.device)\nprint(\"model:\", model.__class__.__name__)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:50.296512Z","iopub.execute_input":"2025-12-26T22:51:50.297110Z","iopub.status.idle":"2025-12-26T22:51:50.628758Z","shell.execute_reply.started":"2025-12-26T22:51:50.297093Z","shell.execute_reply":"2025-12-26T22:51:50.627872Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 7. Loss: noisy-aware (вес + label smoothing)","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\nimport torch\n\ndef ce_smooth(logits, y, smoothing=0.0):\n    if smoothing <= 0:\n        return F.cross_entropy(logits, y, reduction=\"none\")\n    n = logits.size(1)\n    logp = F.log_softmax(logits, dim=1)\n    with torch.no_grad():\n        true = torch.zeros_like(logp).fill_(smoothing / (n - 1))\n        true.scatter_(1, y.unsqueeze(1), 1 - smoothing)\n    return -(true * logp).sum(dim=1)\n\ndef batch_loss(logits, y, w, is_noisy):\n    is_noisy = is_noisy.bool()\n    loss = torch.zeros_like(w)\n\n    if (~is_noisy).any():\n        loss[~is_noisy] = ce_smooth(logits[~is_noisy], y[~is_noisy], smoothing=0.0)\n    if is_noisy.any():\n        loss[is_noisy] = ce_smooth(logits[is_noisy], y[is_noisy], smoothing=CFG.noisy_label_smooth)\n\n    return (loss * w).mean()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:50.630111Z","iopub.execute_input":"2025-12-26T22:51:50.630327Z","iopub.status.idle":"2025-12-26T22:51:50.636651Z","shell.execute_reply.started":"2025-12-26T22:51:50.630308Z","shell.execute_reply":"2025-12-26T22:51:50.635917Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 8. Обучение/валидация: mAP@3 + AMP + AdamW + Cosine","metadata":{}},{"cell_type":"code","source":"import numpy as np\n\ndef mapk3_score(y_true, y_pred_logits, k=3):\n    preds = np.argsort(-y_pred_logits, axis=1)[:, :k]\n    gains = []\n    for t, p in zip(y_true, preds):\n        if t in p:\n            rank = np.where(p == t)[0][0] + 1\n            gains.append(1.0 / rank)\n        else:\n            gains.append(0.0)\n    return float(np.mean(gains))\n\n# torch 2.6+ AMP\nscaler = torch.amp.GradScaler('cuda', enabled=(CFG.device==\"cuda\"))\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.wd)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs)\n\ndef run_epoch(loader, train=True):\n    model.train(train)\n    total_loss = 0.0\n    y_true, y_pred = [], []\n\n    for x, y, w, is_noisy in loader:\n        x = x.to(CFG.device, non_blocking=True)\n        y = y.to(CFG.device, non_blocking=True)\n        w = w.to(CFG.device, non_blocking=True)\n        is_noisy = is_noisy.to(CFG.device, non_blocking=True)\n\n        if train:\n            optimizer.zero_grad(set_to_none=True)\n\n        with torch.amp.autocast(device_type='cuda', enabled=(CFG.device==\"cuda\")):\n            logits = model(x)\n            loss = batch_loss(logits, y, w, is_noisy)\n\n        if train:\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n        total_loss += loss.item() * x.size(0)\n        y_true.append(y.detach().cpu().numpy())\n        y_pred.append(logits.detach().cpu().numpy())\n\n    y_true = np.concatenate(y_true)\n    y_pred = np.concatenate(y_pred)\n    return total_loss / len(loader.dataset), mapk3_score(y_true, y_pred, 3)\n\nbest = -1.0\nfor epoch in range(1, CFG.epochs + 1):\n    tr_loss, tr_map3 = run_epoch(train_loader, True)\n    va_loss, va_map3 = run_epoch(valid_loader, False)\n    scheduler.step()\n\n    print(f\"Epoch {epoch:02d} | tr_loss {tr_loss:.4f} map3 {tr_map3:.4f} | va_loss {va_loss:.4f} map3 {va_map3:.4f}\")\n\n    if va_map3 > best:\n        best = va_map3\n        torch.save({\"model\": model.state_dict(), \"classes\": CLASSES}, \"best_model.pt\")\n\nprint(\"Best val mAP@3:\", best)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T22:51:50.637547Z","iopub.execute_input":"2025-12-26T22:51:50.637890Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 9. Инференс с TTA (3 кропа: начало/центр/конец)","metadata":{}},{"cell_type":"code","source":"def make_crops(wav: np.ndarray, target: int):\n    if len(wav) <= target:\n        return [fix_length(wav, target, train=False)]\n    starts = [0, (len(wav) - target)//2, len(wav) - target]\n    return [wav[s:s+target] for s in starts]\n\n@torch.no_grad()\ndef predict_file_tta(path: str) -> str:\n    wav = load_audio_mono(path)\n    crops = make_crops(wav, TARGET_SAMPLES)\n\n    logits_all = []\n    for c in crops:\n        spec = wav_to_logmel(c)\n        x = torch.from_numpy(spec).unsqueeze(0).unsqueeze(0).to(CFG.device)  # [1,1,M,T]\n        logits = model(x).float().cpu().numpy()[0]\n        logits_all.append(logits)\n\n    logits_mean = np.mean(logits_all, axis=0)\n    top3 = np.argsort(-logits_mean)[:3]\n    return \" \".join([CLASSES[i] for i in top3])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Шаг 10. Формирование submission.csv","metadata":{}},{"cell_type":"code","source":"state = torch.load(\"best_model.pt\", map_location=CFG.device)\nmodel.load_state_dict(state[\"model\"])\nCLASSES = state[\"classes\"]\nmodel.eval()\n\nsub = pd.read_csv(SAMPLE_SUB)\n\nsub[\"label\"] = [predict_file_tta(str(TEST_DIR / f)) for f in sub[\"fname\"]]\nsub.to_csv(\"submission.csv\", index=False)\n\nprint(\"saved:\", \"submission.csv\")\nsub.head()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}