{"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":"nvidiaTeslaT4","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":"markdown","source":"# Классификация животных в тропическом лесу (RFCx)","metadata":{}},{"cell_type":"code","source":"try:\n    import timm\nexcept ImportError:\n    !pip -q install timm\n    import timm\n\nimport os, random, copy, warnings\nfrom dataclasses import dataclass\nfrom concurrent.futures import ThreadPoolExecutor\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\nimport torchaudio\nimport torchaudio.functional as AF\n\nfrom sklearn.model_selection import KFold\nfrom tqdm.auto import tqdm\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.507734Z","iopub.execute_input":"2025-12-27T19:33:59.508431Z","iopub.status.idle":"2025-12-27T19:33:59.514676Z","shell.execute_reply.started":"2025-12-27T19:33:59.508392Z","shell.execute_reply":"2025-12-27T19:33:59.513905Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@dataclass(frozen=True)\nclass CFG:\n    seed: int = 563\n    sr: int = 48_000\n    win_sec: int = 10\n    win_len: int = sr * win_sec\n\n    img_h: int = 224\n    img_w: int = 400\n\n    n_mels: int = 128\n    n_fft: int = 2048\n    hop_length: int = 512\n    top_db: float = 80.0\n\n    epochs: int = 20\n    batch_size: int = 8\n    lr: float = 2e-4\n    wd: float = 1e-2\n    n_folds: int = 5\n    num_workers: int = 2\n\n    cache_images: bool = True\n    backbone: str = \"resnet50\"\n    pretrained: bool = True\n\n    hop_test_sec: int = 10  # шаг сегмента на тесте (10 = без перекрытия)\n\ncfg = CFG()\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\nseed_everything(cfg.seed)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"✅ device:\", device)\nprint(\"torch:\", torch.__version__, \"| torchaudio:\", torchaudio.__version__)\n\n# ----------------------------\n# 2) Data\n# ----------------------------\nDATA_DIR = \"/kaggle/input/rfcx-species-audio-detection\"\nTRAIN_CSV = os.path.join(DATA_DIR, \"train_tp.csv\")\nTRAIN_AUDIO_DIR = os.path.join(DATA_DIR, \"train\")\nTEST_AUDIO_DIR = os.path.join(DATA_DIR, \"test\")\nSUB_CSV = os.path.join(DATA_DIR, \"sample_submission.csv\")\n\ntrain_tp = pd.read_csv(TRAIN_CSV)\nsub = pd.read_csv(SUB_CSV)\n\nlabels = 24\nspecies_cols = [c for c in sub.columns if c != \"recording_id\"]\n\n# как в твоём хорошем ноуте: fmin/fmax из разметки + pad\nfmin_hz = int(train_tp[\"f_min\"].min() * 0.90)\nfmax_hz = int(train_tp[\"f_max\"].max() * 1.10)\n\nprint(\"✅ train_tp:\", train_tp.shape, \"| fmin/fmax:\", fmin_hz, fmax_hz)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.515743Z","iopub.execute_input":"2025-12-27T19:33:59.516011Z","iopub.status.idle":"2025-12-27T19:33:59.556134Z","shell.execute_reply.started":"2025-12-27T19:33:59.515990Z","shell.execute_reply":"2025-12-27T19:33:59.555380Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_audio(recording_id: str, audio_dir: str) -> torch.Tensor:\n    path = os.path.join(audio_dir, f\"{recording_id}.flac\")\n    wav, sr0 = torchaudio.load(path)   # [ch, T]\n    wav = wav.mean(dim=0)              # mono [T]\n    if sr0 != cfg.sr:\n        wav = AF.resample(wav, sr0, cfg.sr)\n    wav = torch.nan_to_num(wav, nan=0.0, posinf=0.0, neginf=0.0)\n    return wav\n\ndef slice_centered(wav: torch.Tensor, t_min_s: float, t_max_s: float) -> torch.Tensor:\n    center = 0.5 * (float(t_min_s) + float(t_max_s))\n    center_i = int(round(center * cfg.sr))\n    start = max(center_i - cfg.win_len // 2, 0)\n    end = min(start + cfg.win_len, wav.numel())\n    start = max(end - cfg.win_len, 0)\n\n    clip = wav[start:end]\n    if clip.numel() < cfg.win_len:\n        clip = F.pad(clip, (0, cfg.win_len - clip.numel()))\n    return clip\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.557042Z","iopub.execute_input":"2025-12-27T19:33:59.557296Z","iopub.status.idle":"2025-12-27T19:33:59.564159Z","shell.execute_reply.started":"2025-12-27T19:33:59.557274Z","shell.execute_reply":"2025-12-27T19:33:59.563491Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mel = torchaudio.transforms.MelSpectrogram(\n    sample_rate=cfg.sr,\n    n_fft=cfg.n_fft,\n    hop_length=cfg.hop_length,\n    n_mels=cfg.n_mels,\n    f_min=fmin_hz,\n    f_max=fmax_hz,\n    power=2.0,\n)\n\ndef power_to_db_like_librosa(S: torch.Tensor, top_db: float = 80.0) -> torch.Tensor:\n    # S: [M, T] power\n    S = torch.clamp(S, min=1e-10)\n    db = 10.0 * torch.log10(S)\n    db = db - db.max()             # максимум = 0\n    db = torch.clamp(db, min=-top_db, max=0.0)\n    return db\n\n@torch.no_grad()\ndef clip_to_image_uint8(wav_clip: torch.Tensor) -> np.ndarray:\n    # wav_clip: [T] on CPU\n    S = mel(wav_clip.unsqueeze(0)).squeeze(0)     # [M, T]\n    db = power_to_db_like_librosa(S, cfg.top_db)  # [-top_db..0]\n\n    # resize через torch (без skimage)\n    x = db.unsqueeze(0).unsqueeze(0)  # [1,1,M,T]\n    x = F.interpolate(x, size=(cfg.img_h, cfg.img_w), mode=\"bilinear\", align_corners=False)\n    x = x.squeeze(0).squeeze(0)       # [H,W]\n\n    # mean/std then minmax -> uint8 (как у тебя)\n    eps = 1e-6\n    x = (x - x.mean()) / (x.std() + eps)\n    x_min = x.min()\n    x_max = x.max()\n    x = (x - x_min) / (x_max - x_min + eps)       # [0..1]\n    img = (x * 255.0).clamp(0,255).byte().cpu().numpy()\n    return img\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.565527Z","iopub.execute_input":"2025-12-27T19:33:59.565788Z","iopub.status.idle":"2025-12-27T19:33:59.586408Z","shell.execute_reply.started":"2025-12-27T19:33:59.565758Z","shell.execute_reply":"2025-12-27T19:33:59.585813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ImgAug:\n    def __init__(self, p=0.7):\n        self.p = p\n\n    def __call__(self, img_u8: np.ndarray) -> np.ndarray:\n        if random.random() > self.p:\n            return img_u8\n\n        x = img_u8.astype(np.float32) / 255.0\n\n        # random gamma\n        if random.random() < 0.5:\n            g = np.random.uniform(0.7, 1.4)\n            x = np.clip(x, 0, 1) ** g\n\n        # random noise\n        if random.random() < 0.5:\n            n = np.random.normal(0, 0.02, size=x.shape).astype(np.float32)\n            x = np.clip(x + n, 0, 1)\n\n        # time flip (горизонтальный)\n        if random.random() < 0.3:\n            x = x[:, ::-1]\n\n        return (x * 255.0).astype(np.uint8)\n\naugment = ImgAug(p=0.7)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.587085Z","iopub.execute_input":"2025-12-27T19:33:59.587302Z","iopub.status.idle":"2025-12-27T19:33:59.596251Z","shell.execute_reply.started":"2025-12-27T19:33:59.587283Z","shell.execute_reply":"2025-12-27T19:33:59.595572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------------\nspec_cache = {}\n\ndef make_one(idx: int):\n    row = train_tp.iloc[idx]\n    wav = load_audio(row[\"recording_id\"], TRAIN_AUDIO_DIR)\n    clip = slice_centered(wav, row[\"t_min\"], row[\"t_max\"])\n    img = clip_to_image_uint8(clip)\n    return idx, img\n\nif cfg.cache_images:\n    print(\"⚙️ Caching mel images...\")\n    with ThreadPoolExecutor() as ex:\n        res = list(tqdm(ex.map(make_one, range(len(train_tp))), total=len(train_tp)))\n    spec_cache = {i: img for i, img in res}\n    print(\"✅ cached:\", len(spec_cache))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:33:59.597212Z","iopub.execute_input":"2025-12-27T19:33:59.597516Z","iopub.status.idle":"2025-12-27T19:35:18.740185Z","shell.execute_reply.started":"2025-12-27T19:33:59.597489Z","shell.execute_reply":"2025-12-27T19:35:18.739599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class RFCXEventDataset(Dataset):\n    def __init__(self, idxs, train: bool):\n        self.idxs = np.asarray(idxs)\n        self.train = train\n\n    def __len__(self):\n        return len(self.idxs)\n\n    def __getitem__(self, k):\n        i = int(self.idxs[k])\n        row = train_tp.iloc[i]\n        y = int(row[\"species_id\"])\n\n        if cfg.cache_images:\n            img = spec_cache[i]\n        else:\n            wav = load_audio(row[\"recording_id\"], TRAIN_AUDIO_DIR)\n            clip = slice_centered(wav, row[\"t_min\"], row[\"t_max\"])\n            img = clip_to_image_uint8(clip)\n\n        if self.train:\n            img = augment(img)\n\n        # 3 канала как в твоём рабочем ноуте\n        x = np.stack([img, img, img], axis=0).astype(np.float32) / 255.0\n        return torch.from_numpy(x), torch.tensor(y, dtype=torch.long) \ndef get_model():\n    try:\n        m = timm.create_model(cfg.backbone, pretrained=cfg.pretrained, num_classes=labels, in_chans=3)\n    except Exception:\n        print(\"⚠️ pretrained failed -> pretrained=False\")\n        m = timm.create_model(cfg.backbone, pretrained=False, num_classes=labels, in_chans=3)\n    return m.to(device)\n\nloss_fn = nn.CrossEntropyLoss()\n\n@torch.no_grad()\ndef evaluate(model, loader):\n    model.eval()\n    total_loss, total_correct, total = 0.0, 0, 0\n    for x, y in loader:\n        x = x.to(device, dtype=torch.float32, non_blocking=True)\n        y = y.to(device, non_blocking=True)\n\n        logits = model(x)\n        loss = loss_fn(logits, y)\n\n        total_loss += loss.item() * x.size(0)\n        total_correct += (logits.argmax(1) == y).sum().item()\n        total += x.size(0)\n\n    return total_loss / max(total, 1), total_correct / max(total, 1)\n\ndef train_fold(fold, tr_idx, va_idx, out_dir=\"models_timm_clean\"):\n    os.makedirs(out_dir, exist_ok=True)\n\n    ds_tr = RFCXEventDataset(tr_idx, train=True)\n    ds_va = RFCXEventDataset(va_idx, train=False)\n\n    dl_tr = DataLoader(ds_tr, batch_size=cfg.batch_size, shuffle=True,\n                       num_workers=cfg.num_workers, pin_memory=True, drop_last=True)\n    dl_va = DataLoader(ds_va, batch_size=64, shuffle=False,\n                       num_workers=cfg.num_workers, pin_memory=True, drop_last=False)\n\n    model = get_model()\n    opt = torch.optim.Adam(model.parameters(), lr=cfg.lr)\n    sch = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, mode=\"min\", patience=3)\n\n    best_acc = -1.0\n    best_wts = copy.deepcopy(model.state_dict())\n    save_path = os.path.join(out_dir, f\"fold{fold}.pt\")\n\n    for epoch in range(1, cfg.epochs + 1):\n        model.train()\n        run_loss, seen = 0.0, 0\n\n        for x, y in dl_tr:\n            x = x.to(device, dtype=torch.float32, non_blocking=True)\n            y = y.to(device, non_blocking=True)\n\n            opt.zero_grad(set_to_none=True)\n            logits = model(x)\n            loss = loss_fn(logits, y)\n            loss.backward()\n            nn.utils.clip_grad_norm_(model.parameters(), 5.0)\n            opt.step()\n\n            run_loss += loss.item() * x.size(0)\n            seen += x.size(0)\n\n        tr_loss = run_loss / max(seen, 1)\n        va_loss, va_acc = evaluate(model, dl_va)\n        sch.step(va_loss)\n\n        print(f\"[Fold {fold}] Epoch {epoch:02d}/{cfg.epochs} | train_loss={tr_loss:.4f} | val_loss={va_loss:.4f} | val_acc={va_acc:.4f}\")\n\n        if va_acc > best_acc:\n            best_acc = va_acc\n            best_wts = copy.deepcopy(model.state_dict())\n\n    model.load_state_dict(best_wts)\n    torch.save(model.state_dict(), save_path)\n    print(f\"✅ Fold {fold} best val_acc={best_acc:.4f} | saved: {save_path}\")\n    return save_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:35:18.741643Z","iopub.execute_input":"2025-12-27T19:35:18.741920Z","iopub.status.idle":"2025-12-27T19:35:18.756541Z","shell.execute_reply.started":"2025-12-27T19:35:18.741899Z","shell.execute_reply":"2025-12-27T19:35:18.755737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"kf = KFold(n_splits=cfg.n_folds, shuffle=True, random_state=cfg.seed)\nX = np.arange(len(train_tp))\ny = train_tp[\"species_id\"].values\n\nfold_models = []\nfor fold, (tr_idx, va_idx) in enumerate(kf.split(X, y)):\n    print(\"=\" * 80)\n    print(f\"🚀 Start Fold {fold}/{cfg.n_folds-1}\")\n    fold_models.append(train_fold(fold, tr_idx, va_idx))\n\nprint(\"✅ trained folds:\", fold_models)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-27T19:35:18.757304Z","iopub.execute_input":"2025-12-27T19:35:18.757580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.no_grad()\ndef load_test_wav(path: str) -> torch.Tensor:\n    wav, sr0 = torchaudio.load(path)\n    wav = wav.mean(dim=0)\n    if sr0 != cfg.sr:\n        wav = AF.resample(wav, sr0, cfg.sr)\n    wav = torch.nan_to_num(wav, nan=0.0, posinf=0.0, neginf=0.0)\n    return wav\n\n@torch.no_grad()\ndef wav_to_segments(wav: torch.Tensor) -> list[torch.Tensor]:\n    hop = cfg.hop_test_sec * cfg.sr\n    segs = []\n    for start in range(0, max(1, wav.numel()), hop):\n        clip = wav[start:start + cfg.win_len]\n        if clip.numel() < cfg.win_len:\n            clip = F.pad(clip, (0, cfg.win_len - clip.numel()))\n        segs.append(clip)\n        if start + cfg.win_len >= wav.numel():\n            break\n    return segs\n\n@torch.no_grad()\ndef predict_one_file(models, wav: torch.Tensor) -> np.ndarray:\n    segs = wav_to_segments(wav)\n\n    # готовим батчами, чтобы не держать всё сразу\n    all_model_preds = []\n\n    for model in models:\n        model.eval()\n        best = torch.zeros(labels, device=device)\n\n        bs = 32\n        for i in range(0, len(segs), bs):\n            batch = segs[i:i+bs]\n            imgs = []\n            for clip in batch:\n                img = clip_to_image_uint8(clip)  # uint8 HxW\n                x = np.stack([img, img, img], axis=0).astype(np.float32) / 255.0\n                imgs.append(x)\n            xb = torch.from_numpy(np.stack(imgs, axis=0)).to(device)\n\n            logits = model(xb)\n            probs = torch.softmax(logits, dim=1)           # [B,24]\n            best = torch.maximum(best, probs.max(dim=0).values)\n\n        all_model_preds.append(best)\n\n    avg = torch.stack(all_model_preds, dim=0).mean(dim=0)\n    return avg.detach().cpu().numpy()\n\ndef load_members(paths):\n    ms = []\n    for p in paths:\n        m = get_model()\n        m.load_state_dict(torch.load(p, map_location=device))\n        m.eval()\n        ms.append(m)\n    return ms\n\nmembers = load_members(fold_models)\nprint(\"✅ Loaded members:\", len(members))\n\nrows = []\ntest_files = sorted(os.listdir(TEST_AUDIO_DIR))\nfor tf in tqdm(test_files, desc=\"Predicting test\"):\n    rid = os.path.splitext(tf)[0]\n    wav = load_test_wav(os.path.join(TEST_AUDIO_DIR, tf))\n    probs = predict_one_file(members, wav)\n    rows.append([rid] + probs.tolist())\n\nout = pd.DataFrame(rows, columns=[\"recording_id\"] + species_cols)\nout.to_csv(\"submission.csv\", index=False)\nprint(\"✅ Saved submission.csv:\", out.shape)\nout.head()","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}