{"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":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q efficientnet_pytorch --no-deps\n\nimport os\nimport gc\nimport cv2\nimport csv\nimport time\nimport torch\nimport random\nimport librosa\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport librosa.display\n\nfrom pathlib import Path\nfrom tqdm.notebook import tqdm\nfrom dataclasses import dataclass\nfrom torch.optim import Adam\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom sklearn.model_selection import KFold\n\nfrom efficientnet_pytorch import EfficientNet\n\nplt.style.use('ggplot')\npd.set_option('display.max_columns', None)\n\ndef seed_everything(seed=42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nseed_everything(42)\nprint(\"Environment Ready.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:36:58.026171Z","iopub.execute_input":"2026-01-21T10:36:58.026452Z","iopub.status.idle":"2026-01-21T10:37:09.012378Z","shell.execute_reply.started":"2026-01-21T10:36:58.026419Z","shell.execute_reply":"2026-01-21T10:37:09.011565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@dataclass\nclass Config:\n    sr: int = 48_000\n    duration: int = 10\n    fmin: int = 40\n    fmax: int = 16_000\n    n_mels: int = 128\n    \n    num_classes: int = 24\n    model_name: str = 'efficientnet-b0'\n    image_size: tuple = (224, 512) # Height, Width\n    \n    n_folds: int = 5\n    epochs: int = 18\n    batch_size: int = 8\n    lr: float = 1e-3\n    \n    root_dir: Path = Path(\"/kaggle/input/rfcx-species-audio-detection\")\n    train_dir: Path = root_dir / \"train\"\n    test_dir: Path = root_dir / \"test\"\n    device: torch.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\ncfg = Config()\nprint(f\"Configuration loaded. Device: {cfg.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:37:09.014069Z","iopub.execute_input":"2026-01-21T10:37:09.014469Z","iopub.status.idle":"2026-01-21T10:37:09.075252Z","shell.execute_reply.started":"2026-01-21T10:37:09.014441Z","shell.execute_reply":"2026-01-21T10:37:09.074316Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_tp = pd.read_csv(cfg.root_dir / \"train_tp.csv\")\n\nplt.figure(figsize=(15, 6))\nsns.countplot(data=df_tp, x='species_id', palette='viridis')\nplt.title(\"Distribution of Species ID in Training Data\")\nplt.xlabel(\"Species ID\")\nplt.ylabel(\"Count\")\nplt.show()\n\ndef visualize_sample(index, df, cfg):\n    row = df.iloc[index]\n    path = cfg.train_dir / f\"{row['recording_id']}.flac\"\n    \n    audio, _ = librosa.load(path, sr=cfg.sr)\n    \n    center_sec = (row['t_min'] + row['t_max']) / 2\n    start_sample = max(0, int((center_sec - cfg.duration/2) * cfg.sr))\n    end_sample = start_sample + int(cfg.duration * cfg.sr)\n    chunk = audio[start_sample:end_sample]\n    \n    melspec = librosa.feature.melspectrogram(\n        y=chunk, sr=cfg.sr, n_mels=cfg.n_mels, fmin=cfg.fmin, fmax=cfg.fmax\n    )\n    melspec_db = librosa.power_to_db(melspec, top_db=80)\n\n    plt.figure(figsize=(12, 4))\n    librosa.display.specshow(melspec_db, sr=cfg.sr, x_axis='time', y_axis='mel', \n                             fmin=cfg.fmin, fmax=cfg.fmax)\n    plt.colorbar(format='%+2.0f dB')\n    plt.title(f\"Mel-Spectrogram: Species {row['species_id']} (Rec: {row['recording_id']})\")\n    plt.tight_layout()\n    plt.show()\n\nprint(f\"Total True Positive Samples: {len(df_tp)}\")\nvisualize_sample(0, df_tp, cfg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:37:09.076269Z","iopub.execute_input":"2026-01-21T10:37:09.076867Z","iopub.status.idle":"2026-01-21T10:37:23.137264Z","shell.execute_reply.started":"2026-01-21T10:37:09.076833Z","shell.execute_reply":"2026-01-21T10:37:23.136321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class AudioAugmentations:\n    @staticmethod\n    def apply(image: np.ndarray) -> np.ndarray:\n        if np.random.rand() < 0.3:\n            noise = np.random.randn(*image.shape) * 0.05\n            image = image + noise\n            \n        if np.random.rand() < 0.3:\n            factor = np.random.uniform(0.8, 1.2)\n            mean = image.mean()\n            image = (image - mean) * factor + mean\n            \n        return image\n\nclass RainforestDataset(Dataset):\n    def __init__(self, df: pd.DataFrame, cfg: Config, mode: str = 'train'):\n        self.df = df\n        self.cfg = cfg\n        self.mode = mode\n        self.file_paths = {\n            rec_id: cfg.train_dir / f\"{rec_id}.flac\" \n            for rec_id in df['recording_id'].values\n        }\n        \n    def __len__(self):\n        return len(self.df)\n        \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        rec_id = row['recording_id']\n        path = self.file_paths[rec_id]\n        target_len = self.cfg.duration * self.cfg.sr\n        \n        if self.mode == 'train':\n            center_sec = (row['t_min'] + row['t_max']) / 2\n            \n            offset = max(0, center_sec - self.cfg.duration / 2)\n            audio, _ = librosa.load(path, sr=self.cfg.sr, offset=offset, duration=self.cfg.duration)\n            \n            if len(audio) < target_len:\n                audio = np.pad(audio, (0, target_len - len(audio)))\n            else:\n                audio = audio[:target_len]\n                \n        else:\n            audio, _ = librosa.load(path, sr=self.cfg.sr, duration=self.cfg.duration)\n            if len(audio) < target_len:\n                audio = np.pad(audio, (0, target_len - len(audio)))\n            audio = audio[:target_len]\n\n        melspec = librosa.feature.melspectrogram(\n            y=audio, sr=self.cfg.sr, n_mels=self.cfg.n_mels, \n            fmin=self.cfg.fmin, fmax=self.cfg.fmax\n        )\n        melspec = librosa.power_to_db(melspec, top_db=80)\n\n        melspec = (melspec - melspec.min()) / (melspec.max() - melspec.min() + 1e-6)\n        \n        melspec = torch.tensor(melspec).unsqueeze(0).unsqueeze(0) # (1, 1, H, W)\n        melspec = torch.nn.functional.interpolate(\n            melspec, size=(self.cfg.image_size[0], self.cfg.image_size[1]), \n            mode='bilinear', align_corners=False\n        ).squeeze()\n        \n        image = melspec.numpy()\n        \n        if self.mode == 'train':\n            image = AudioAugmentations.apply(image)\n            \n        # Stack to 3 channels for EfficientNet\n        image = np.stack([image, image, image])\n        image = torch.tensor(image, dtype=torch.float32)\n        \n        label = torch.tensor(row['species_id'], dtype=torch.long)\n        \n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:37:23.138355Z","iopub.execute_input":"2026-01-21T10:37:23.138707Z","iopub.status.idle":"2026-01-21T10:37:23.149983Z","shell.execute_reply.started":"2026-01-21T10:37:23.138683Z","shell.execute_reply":"2026-01-21T10:37:23.149242Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_epoch(model, loader, optimizer, criterion, device):\n    model.train()\n    running_loss = 0.0\n    \n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        running_loss += loss.item() * images.size(0)\n        \n    return running_loss / len(loader.dataset)\n\n@torch.no_grad()\ndef valid_epoch(model, loader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    correct_preds = 0\n    total_preds = 0\n    \n    for images, labels in loader:\n        images, labels = images.to(device), labels.to(device)\n        \n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        \n        running_loss += loss.item() * images.size(0)\n        \n        preds = outputs.argmax(dim=1)\n        correct_preds += (preds == labels).sum().item()\n        total_preds += labels.size(0)\n        \n    return running_loss / len(loader.dataset), correct_preds / total_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:37:23.151598Z","iopub.execute_input":"2026-01-21T10:37:23.151911Z","iopub.status.idle":"2026-01-21T10:37:23.179114Z","shell.execute_reply.started":"2026-01-21T10:37:23.151885Z","shell.execute_reply":"2026-01-21T10:37:23.178504Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\", message=\"PySoundFile failed\")\nwarnings.filterwarnings(\"ignore\", message=\"librosa.core.audio.__audioread_load\")\n\ndf = pd.read_csv(cfg.root_dir / \"train_tp.csv\")\nkfold = KFold(n_splits=cfg.n_folds, shuffle=True, random_state=42)\n\nfor fold, (train_idx, val_idx) in enumerate(kfold.split(df)):\n    print(f\"\\n{'='*20} Fold {fold+1}/{cfg.n_folds} {'='*20}\")\n    \n    train_ds = RainforestDataset(df.iloc[train_idx], cfg, mode='train')\n    val_ds = RainforestDataset(df.iloc[val_idx], cfg, mode='valid')\n    \n    train_loader = DataLoader(train_ds, batch_size=cfg.batch_size, shuffle=True, \n                              num_workers=2, pin_memory=True, drop_last=True)\n    val_loader = DataLoader(val_ds, batch_size=cfg.batch_size, shuffle=False, \n                            num_workers=2, pin_memory=True)\n    \n    model = EfficientNet.from_pretrained(cfg.model_name, num_classes=cfg.num_classes)\n    model = model.to(cfg.device)\n    \n    optimizer = Adam(model.parameters(), lr=cfg.lr)\n    scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)\n    criterion = nn.CrossEntropyLoss()\n    \n    best_acc = 0.0\n    history = {'train_loss': [], 'val_loss': [], 'val_acc': []}\n    \n    for epoch in range(cfg.epochs):\n        train_loss = train_epoch(model, train_loader, optimizer, criterion, cfg.device)\n        val_loss, val_acc = valid_epoch(model, val_loader, criterion, cfg.device)\n        \n        scheduler.step(val_loss)\n        \n        history['train_loss'].append(train_loss)\n        history['val_loss'].append(val_loss)\n        history['val_acc'].append(val_acc)\n        \n        print(f\"Epoch {epoch+1:02d} | T_Loss: {train_loss:.4f} | V_Loss: {val_loss:.4f} | V_Acc: {val_acc:.4f}\")\n        \n        if val_acc > best_acc:\n            best_acc = val_acc\n            torch.save(model.state_dict(), f\"model_fold_{fold}.pth\")\n            \n    plt.figure(figsize=(10, 4))\n    plt.plot(history['train_loss'], label='Train Loss')\n    plt.plot(history['val_loss'], label='Val Loss')\n    plt.title(f'Fold {fold+1} Loss Curve')\n    plt.legend()\n    plt.show()\n    \n    del model, optimizer, train_loader, val_loader\n    gc.collect()\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T10:45:57.404012Z","iopub.execute_input":"2026-01-21T10:45:57.404899Z","iopub.status.idle":"2026-01-21T12:45:41.467696Z","shell.execute_reply.started":"2026-01-21T10:45:57.404861Z","shell.execute_reply":"2026-01-21T12:45:41.466929Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_files = [f.name.split('.')[0] for f in cfg.test_dir.glob(\"*.flac\")]\n\nclass TestDataset(Dataset):\n    def __init__(self, file_list, cfg):\n        self.file_list = file_list\n        self.cfg = cfg\n        self.target_len = cfg.duration * cfg.sr \n        \n    def __len__(self):\n        return len(self.file_list)\n        \n    def __getitem__(self, idx):\n        rec_id = self.file_list[idx]\n        path = self.cfg.test_dir / f\"{rec_id}.flac\"\n        \n        audio, _ = librosa.load(path, sr=self.cfg.sr)\n        \n        segments = []\n        num_segments = int(np.ceil(len(audio) / self.target_len))\n        num_segments = min(num_segments, 6) \n        \n        for i in range(num_segments):\n            start = i * self.target_len\n            end = start + self.target_len\n            if end > len(audio): break # Ignore incomplete last chunk if strictly enforcing size\n            \n            chunk = audio[start:end]\n            \n            melspec = librosa.feature.melspectrogram(\n                y=chunk, sr=self.cfg.sr, n_mels=self.cfg.n_mels, \n                fmin=self.cfg.fmin, fmax=self.cfg.fmax\n            )\n            melspec = librosa.power_to_db(melspec, top_db=80)\n            melspec = (melspec - melspec.min()) / (melspec.max() - melspec.min() + 1e-6)\n            \n            img = cv2.resize(melspec, (self.cfg.image_size[1], self.cfg.image_size[0]))\n            img = np.stack([img, img, img])\n            segments.append(img)\n            \n        if not segments:\n            return torch.zeros((1, 3, self.cfg.image_size[0], self.cfg.image_size[1])), rec_id\n            \n        return np.stack(segments), rec_id\n\ntest_ds = TestDataset(test_files, cfg)\ntest_loader = DataLoader(test_ds, batch_size=1, shuffle=False, num_workers=2)\n\nmodels = []\nfor i in range(cfg.n_folds):\n    path = f\"model_fold_{i}.pth\"\n    if os.path.exists(path):\n        m = EfficientNet.from_pretrained(cfg.model_name, num_classes=cfg.num_classes)\n        m.load_state_dict(torch.load(path, map_location=cfg.device))\n        m.to(cfg.device)\n        m.eval()\n        models.append(m)\n\nprint(f\"Loaded {len(models)} models for inference.\")\n\nresults = {}\nwith torch.no_grad():\n    for batch_segments, rec_ids in tqdm(test_loader, desc=\"Inference\"):\n        inputs = batch_segments.squeeze(0).float().to(cfg.device)\n        rec_id = rec_ids[0]\n        \n        fold_preds = []\n        for model in models:\n            logits = model(inputs)\n            max_logits, _ = torch.max(logits, dim=0) \n            probs = torch.sigmoid(max_logits)\n            fold_preds.append(probs.cpu().numpy())\n        \n        avg_pred = np.mean(fold_preds, axis=0)\n        results[rec_id] = avg_pred\n\nwith open('submission.csv', 'w', newline='') as f:\n    writer = csv.writer(f)\n    head = ['recording_id'] + [f's{i}' for i in range(24)]\n    writer.writerow(head)\n    \n    for rec_id in test_files:\n        if rec_id in results:\n            row = [rec_id] + list(results[rec_id])\n        else:\n            row = [rec_id] + [0.0]*24\n        writer.writerow(row)\n\nprint(\"Inference Complete. 'submission.csv' saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T13:58:48.890289Z","iopub.execute_input":"2026-01-21T13:58:48.890558Z","iopub.status.idle":"2026-01-21T14:09:38.552835Z","shell.execute_reply.started":"2026-01-21T13:58:48.890528Z","shell.execute_reply":"2026-01-21T14:09:38.551995Z"},"_kg_hide-output":false},"outputs":[],"execution_count":null}]}