{"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"},{"sourceId":699685,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":530843,"modelId":544730}],"dockerImageVersionId":31234,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Классификация произвольных звуков","metadata":{}},{"cell_type":"markdown","source":"## Установка и импорты","metadata":{}},{"cell_type":"code","source":"import os, random, math\nfrom dataclasses import dataclass\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\n# аудио\nimport soundfile as sf\nimport librosa\n\n# модель (spectrogram-as-image)\ntry:\n    import timm\nexcept Exception:\n    timm = None\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import LabelEncoder\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.336819Z","iopub.execute_input":"2025-12-26T21:35:41.337184Z","iopub.status.idle":"2025-12-26T21:35:41.343423Z","shell.execute_reply.started":"2025-12-26T21:35:41.337166Z","shell.execute_reply":"2025-12-26T21:35:41.342207Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Конфиг и пути","metadata":{}},{"cell_type":"code","source":"@dataclass\nclass CFG:\n    seed: int = 42\n\n    # audio\n    sr: int = 32000               # ресемплим с 44.1kHz → 32kHz для скорости\n    clip_seconds: float = 5.0     # фиксированная длина окна (train + TTA)\n    n_mels: int = 128\n    fmin: int = 20\n    fmax: int = 16000\n\n    # spectrogram\n    n_fft: int = 1024\n    hop_length: int = 320         # 10ms @32kHz\n    win_length: int = 1024\n\n    # train\n    batch_size: int = 32\n    num_workers: int = 2\n    epochs: int = 8\n    lr: float = 3e-4\n    weight_decay: float = 1e-2\n    label_smoothing_noisy: float = 0.10  # для noisy (manually_verified=0)\n    noisy_weight: float = 0.5           # уменьшаем вклад noisy в loss\n\n    # model\n    backbone: str = \"efficientnet_b0\"    # timm\n    pretrained: bool = True\n\n    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\ndef seed_everything(seed: int):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = False\n    torch.backends.cudnn.benchmark = True\n\nseed_everything(CFG.seed)\n\n# Kaggle: /kaggle/input/freesound-audio-tagging/\nROOT = Path(\"/kaggle/input/freesound-audio-tagging\")\nif not ROOT.exists():\n    ROOT = Path(\"./freesound-audio-tagging\")  # локально\n\nTRAIN_AUDIO = ROOT / \"audio_train\"\nTEST_AUDIO  = ROOT / \"audio_test\"\nTRAIN_CSV   = ROOT / \"train.csv\"\nSAMPLE_SUB  = ROOT / \"sample_submission.csv\"\n\nprint(\"ROOT:\", ROOT)\nprint(\"train.csv exists:\", TRAIN_CSV.exists())\nprint(\"audio_train exists:\", TRAIN_AUDIO.exists())\nprint(\"audio_test exists:\", TEST_AUDIO.exists())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.344671Z","iopub.execute_input":"2025-12-26T21:35:41.344887Z","iopub.status.idle":"2025-12-26T21:35:41.374842Z","shell.execute_reply.started":"2025-12-26T21:35:41.344868Z","shell.execute_reply":"2025-12-26T21:35:41.372783Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Загрузка метаданных + EDA","metadata":{}},{"cell_type":"code","source":"df = pd.read_csv(TRAIN_CSV)\ndisplay(df.head())\nprint(df.columns.tolist())\nprint(\"rows:\", len(df))\nprint(df['manually_verified'].value_counts(dropna=False))\n\nlabel_counts = df['label'].value_counts()\ndisplay(label_counts.head(10))\nprint(\"num_classes:\", df['label'].nunique())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.375936Z","iopub.execute_input":"2025-12-26T21:35:41.376148Z","iopub.status.idle":"2025-12-26T21:35:41.413787Z","shell.execute_reply.started":"2025-12-26T21:35:41.376132Z","shell.execute_reply":"2025-12-26T21:35:41.412526Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Быстрый просмотр аудио (длительность)","metadata":{}},{"cell_type":"code","source":"def audio_info(path: Path):\n    y, sr = sf.read(path)\n    if y.ndim > 1:\n        y = y.mean(axis=1)\n    return len(y)/sr, sr\n\nsample_files = df.sample(5, random_state=CFG.seed)['fname'].tolist()\nfor fn in sample_files:\n    dur, sr0 = audio_info(TRAIN_AUDIO / fn)\n    print(fn, \"dur_s=\", round(dur, 3), \"sr=\", sr0)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.415160Z","iopub.execute_input":"2025-12-26T21:35:41.415498Z","iopub.status.idle":"2025-12-26T21:35:41.445997Z","shell.execute_reply.started":"2025-12-26T21:35:41.415479Z","shell.execute_reply":"2025-12-26T21:35:41.444849Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Препроцессинг: загрузка, кроп/паддинг, log-mel","metadata":{}},{"cell_type":"code","source":"def load_audio_mono(path: Path, target_sr: int) -> np.ndarray:\n    y, sr = sf.read(path, dtype=\"float32\")\n    if y.ndim > 1:\n        y = y.mean(axis=1)\n    if sr != target_sr:\n        y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)\n    return y\n\ndef crop_or_pad(y: np.ndarray, target_len: int, train: bool) -> np.ndarray:\n    if len(y) >= target_len:\n        if train:\n            start = np.random.randint(0, len(y) - target_len + 1)\n        else:\n            start = (len(y) - target_len) // 2\n        y = y[start:start+target_len]\n    else:\n        pad = target_len - len(y)\n        y = np.pad(y, (0, pad), mode=\"constant\")\n    return y\n\ndef log_mel(y: np.ndarray) -> np.ndarray:\n    S = librosa.feature.melspectrogram(\n        y=y,\n        sr=CFG.sr,\n        n_fft=CFG.n_fft,\n        hop_length=CFG.hop_length,\n        win_length=CFG.win_length,\n        n_mels=CFG.n_mels,\n        fmin=CFG.fmin,\n        fmax=CFG.fmax,\n        power=2.0\n    )\n    S_db = librosa.power_to_db(S, ref=np.max)\n    S_db = (S_db - S_db.min()) / (S_db.max() - S_db.min() + 1e-6)  # [0,1]\n    return S_db.astype(np.float32)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.446817Z","iopub.execute_input":"2025-12-26T21:35:41.447025Z","iopub.status.idle":"2025-12-26T21:35:41.455535Z","shell.execute_reply.started":"2025-12-26T21:35:41.447008Z","shell.execute_reply":"2025-12-26T21:35:41.454317Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dataset/Dataloader (учет noisy разметки)","metadata":{}},{"cell_type":"code","source":"le = LabelEncoder()\ndf['target'] = le.fit_transform(df['label'])\nnum_classes = len(le.classes_)\nprint(\"classes:\", num_classes)\n\nverified = df[df['manually_verified'] == 1].copy()\ntrain_v_idx, val_idx = train_test_split(\n    verified.index, test_size=0.20, random_state=CFG.seed, stratify=verified['target']\n)\n\ntrain_df = pd.concat([df.loc[train_v_idx], df[df['manually_verified'] == 0]], ignore_index=True)\nval_df   = df.loc[val_idx].reset_index(drop=True)\n\nprint(\"train:\", len(train_df), \"val:\", len(val_df))\nprint(\"val manually_verified unique:\", val_df['manually_verified'].unique())\n\nclass FreesoundDataset(Dataset):\n    def __init__(self, meta: pd.DataFrame, audio_dir: Path, train: bool):\n        self.meta = meta.reset_index(drop=True)\n        self.audio_dir = audio_dir\n        self.train = train\n        self.target_len = int(CFG.sr * CFG.clip_seconds)\n\n    def __len__(self):\n        return len(self.meta)\n\n    def __getitem__(self, i):\n        row = self.meta.iloc[i]\n        y = load_audio_mono(self.audio_dir / row.fname, CFG.sr)\n        y = crop_or_pad(y, self.target_len, train=self.train)\n\n        # простые waveform-ауги (можно расширять)\n        if self.train:\n            if np.random.rand() < 0.5:\n                y = y * (0.7 + 0.6*np.random.rand())  # random gain\n            if np.random.rand() < 0.3:\n                y = y + 0.005*np.random.randn(len(y)).astype(np.float32)  # noise\n\n        x = log_mel(y)                         # [mels, time]\n        x = torch.from_numpy(x).unsqueeze(0)   # [1, mels, time]\n\n        target = int(row.target)\n        is_noisy = (int(row.manually_verified) == 0)\n        sample_weight = CFG.noisy_weight if is_noisy else 1.0\n\n        return x, target, sample_weight, is_noisy\n\ntrain_ds = FreesoundDataset(train_df, TRAIN_AUDIO, train=True)\nval_ds   = FreesoundDataset(val_df, TRAIN_AUDIO, train=False)\n\ntrain_loader = DataLoader(train_ds, batch_size=CFG.batch_size, shuffle=True,\n                          num_workers=CFG.num_workers, pin_memory=True, drop_last=True)\nval_loader   = DataLoader(val_ds, batch_size=CFG.batch_size*2, shuffle=False,\n                          num_workers=CFG.num_workers, pin_memory=True)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.456532Z","iopub.execute_input":"2025-12-26T21:35:41.456757Z","iopub.status.idle":"2025-12-26T21:35:41.498393Z","shell.execute_reply.started":"2025-12-26T21:35:41.456740Z","shell.execute_reply":"2025-12-26T21:35:41.497270Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Модель: EfficientNet из timm (spectrogram-as-image)","metadata":{}},{"cell_type":"code","source":"import torch\nimport timm\n\nWEIGHTS = \"/kaggle/input/efficientnet-b0/pytorch/default/1/efficientnet_b0_ra_in1k.pth\"\n\n# 1) создаём модель БЕЗ checkpoint_path (иначе strict загрузка и падение)\nmodel = timm.create_model(\n    \"efficientnet_b0.ra_in1k\",\n    pretrained=False,\n    in_chans=1,\n    num_classes=num_classes,   # 41\n).to(CFG.device)\n\n# 2) читаем веса\nckpt = torch.load(WEIGHTS, map_location=\"cpu\")\nstate = ckpt.get(\"state_dict\", ckpt)\n\n# 3) на всякий: убираем префиксы module./model.\nnew_state = {}\nfor k, v in state.items():\n    if k.startswith(\"module.\"):\n        k = k[len(\"module.\"):]\n    if k.startswith(\"model.\"):\n        k = k[len(\"model.\"):]\n    new_state[k] = v\nstate = new_state\n\n# 4) адаптируем первый conv: RGB -> 1 канал (среднее по каналам)\nif \"conv_stem.weight\" in state:\n    w = state[\"conv_stem.weight\"]  # [32,3,3,3]\n    if w.ndim == 4 and w.shape[1] == 3:\n        state[\"conv_stem.weight\"] = w.mean(dim=1, keepdim=True)  # -> [32,1,3,3]\n\n# 5) выкидываем голову ImageNet (1000 классов)\nstate.pop(\"classifier.weight\", None)\nstate.pop(\"classifier.bias\", None)\n\n# 6) грузим нестрого\nmissing, unexpected = model.load_state_dict(state, strict=False)\nprint(\"missing:\", len(missing))\nprint(\"unexpected:\", len(unexpected))\nprint(\"пример missing:\", missing[:5])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.500969Z","iopub.execute_input":"2025-12-26T21:35:41.501819Z","iopub.status.idle":"2025-12-26T21:35:41.615436Z","shell.execute_reply.started":"2025-12-26T21:35:41.501796Z","shell.execute_reply":"2025-12-26T21:35:41.614040Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Метрика mAP@3 (локально)","metadata":{}},{"cell_type":"code","source":"@torch.no_grad()\ndef map_at_3(probs: torch.Tensor, targets: torch.Tensor) -> float:\n    top3 = probs.topk(3, dim=1).indices  # [N,3]\n    score = 0.0\n    for i in range(len(targets)):\n        t = targets[i].item()\n        hits = (top3[i] == t).nonzero(as_tuple=False)\n        if len(hits) > 0:\n            rank = int(hits[0].item())  # 0/1/2\n            score += 1.0 / (rank + 1)\n    return score / len(targets)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.617831Z","iopub.execute_input":"2025-12-26T21:35:41.618203Z","iopub.status.idle":"2025-12-26T21:35:41.625695Z","shell.execute_reply.started":"2025-12-26T21:35:41.618184Z","shell.execute_reply":"2025-12-26T21:35:41.623699Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train/Valid loop (AMP + AdamW + Cosine)","metadata":{}},{"cell_type":"code","source":"def ce_with_optional_smoothing(logits, target, smoothing: float):\n    if smoothing <= 0:\n        return F.cross_entropy(logits, target, reduction=\"none\")\n    n_classes = logits.size(1)\n    log_probs = F.log_softmax(logits, dim=1)\n    with torch.no_grad():\n        true_dist = torch.zeros_like(log_probs)\n        true_dist.fill_(smoothing / (n_classes - 1))\n        true_dist.scatter_(1, target.unsqueeze(1), 1.0 - smoothing)\n    return -(true_dist * log_probs).sum(dim=1)\n\ndef train_one_epoch(model, loader, optimizer, scaler):\n    model.train()\n    losses = []\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).float()\n        is_noisy = is_noisy.to(CFG.device, non_blocking=True)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        with torch.cuda.amp.autocast(enabled=(CFG.device==\"cuda\")):\n            logits = model(x)\n\n            loss_vec = torch.zeros_like(w)\n            mask_noisy = is_noisy.bool()\n            mask_clean = ~mask_noisy\n\n            if mask_clean.any():\n                loss_vec[mask_clean] = ce_with_optional_smoothing(logits[mask_clean], y[mask_clean], smoothing=0.0)\n            if mask_noisy.any():\n                loss_vec[mask_noisy] = ce_with_optional_smoothing(\n                    logits[mask_noisy], y[mask_noisy], smoothing=CFG.label_smoothing_noisy\n                )\n\n            loss = (loss_vec * w).mean()\n\n        scaler.scale(loss).backward()\n        scaler.step(optimizer)\n        scaler.update()\n        losses.append(loss.item())\n\n    return float(np.mean(losses))\n\n@torch.no_grad()\ndef validate(model, loader):\n    model.eval()\n    all_probs, all_t = [], []\n    losses = []\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        with torch.cuda.amp.autocast(enabled=(CFG.device==\"cuda\")):\n            logits = model(x)\n            loss = F.cross_entropy(logits, y)\n        probs = F.softmax(logits.float(), dim=1).cpu()\n        all_probs.append(probs)\n        all_t.append(y.cpu())\n        losses.append(loss.item())\n\n    probs = torch.cat(all_probs, dim=0)\n    t = torch.cat(all_t, dim=0)\n    score = map_at_3(probs, t)\n    return float(np.mean(losses)), float(score)\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=CFG.lr, weight_decay=CFG.weight_decay)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=CFG.epochs)\nscaler = torch.cuda.amp.GradScaler(enabled=(CFG.device==\"cuda\"))\n\nbest_score = -1\nbest_path = \"best_model.pt\"\n\nfor epoch in range(1, CFG.epochs+1):\n    tr_loss = train_one_epoch(model, train_loader, optimizer, scaler)\n    va_loss, va_map3 = validate(model, val_loader)\n    scheduler.step()\n\n    if va_map3 > best_score:\n        best_score = va_map3\n        torch.save({\"model\": model.state_dict(), \"classes\": le.classes_}, best_path)\n\n    print(f\"Epoch {epoch:02d} | train_loss {tr_loss:.4f} | val_loss {va_loss:.4f} | val_mAP@3 {va_map3:.4f} | best {best_score:.4f}\")\n\nprint(\"Best saved to:\", best_path)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-26T21:35:41.627280Z","iopub.execute_input":"2025-12-26T21:35:41.627612Z","execution_failed":"2025-12-26T21:43:53.527Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Инференс + TTA (3 кропа: начало/центр/конец)","metadata":{}},{"cell_type":"code","source":"@torch.no_grad()\ndef predict_file(path: Path, model: nn.Module) -> np.ndarray:\n    model.eval()\n    y = load_audio_mono(path, CFG.sr)\n    target_len = int(CFG.sr * CFG.clip_seconds)\n\n    crops = []\n    if len(y) <= target_len:\n        crops = [crop_or_pad(y, target_len, train=False)]\n    else:\n        starts = [0, (len(y)-target_len)//2, len(y)-target_len]\n        for st in starts:\n            crops.append(y[st:st+target_len])\n\n    probs = []\n    for c in crops:\n        x = torch.from_numpy(log_mel(c)).unsqueeze(0).unsqueeze(0).to(CFG.device)  # [1,1,mels,time]\n        logits = model(x)\n        p = F.softmax(logits.float(), dim=1).cpu().numpy()[0]\n        probs.append(p)\n\n    return np.mean(probs, axis=0)\n\nckpt = torch.load(best_path, map_location=CFG.device, weights_only=False)\nmodel.load_state_dict(ckpt[\"model\"])\nclasses = ckpt.get(\"classes\", None)\n\n","metadata":{"trusted":true,"execution":{"execution_failed":"2025-12-26T21:43:53.528Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submission (top-3 labels)","metadata":{}},{"cell_type":"code","source":"sub = pd.read_csv(SAMPLE_SUB)\ntest_files = sub['fname'].tolist()\n\npred_labels = []\nfor fn in test_files:\n    p = predict_file(TEST_AUDIO / fn, model)\n    top3 = p.argsort()[::-1][:3]\n    pred_labels.append(\" \".join(le.inverse_transform(top3)))\n\nsub['label'] = pred_labels\nsub.to_csv(\"submission.csv\", index=False)\nsub.head()\n","metadata":{"trusted":true,"execution":{"execution_failed":"2025-12-26T21:43:53.528Z"}},"outputs":[],"execution_count":null}]}