{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11750210,"sourceType":"datasetVersion","datasetId":7376624}],"dockerImageVersionId":31012,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nimport glob\nfrom tqdm import tqdm\nfrom torchvision.models import efficientnet_b0\nimport warnings\nwarnings.filterwarnings('ignore')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T18:31:26.092427Z","iopub.execute_input":"2025-05-09T18:31:26.092780Z","iopub.status.idle":"2025-05-09T18:31:35.873139Z","shell.execute_reply.started":"2025-05-09T18:31:26.092746Z","shell.execute_reply":"2025-05-09T18:31:35.872035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Environment Setup\ntorch.manual_seed(42)\ndevice = torch.device(\"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T18:31:48.235253Z","iopub.execute_input":"2025-05-09T18:31:48.235873Z","iopub.status.idle":"2025-05-09T18:31:48.247448Z","shell.execute_reply.started":"2025-05-09T18:31:48.235842Z","shell.execute_reply":"2025-05-09T18:31:48.246465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 2. Precompute RMS for Segment Selection\ndef compute_rms_segments(audio, sr=32000, segment_length=5):\n    \"\"\"Compute RMS for each 5-second segment in audio.\"\"\"\n    segment_samples = segment_length * sr\n    segments = [audio[i:i+segment_samples] for i in range(0, len(audio), segment_samples)]\n    rms_values = [np.sqrt(np.mean(segment**2)) if len(segment) > 0 else 0 for segment in segments]\n    return rms_values\n\ndef precompute_rms(train_df, audio_dir, output_csv='train_segments.csv'):\n    \"\"\"Precompute RMS for all files and save to CSV.\"\"\"\n    print(\"Precomputing RMS for all files...\")\n    rms_data = []\n    for idx, row in tqdm(train_df.iterrows(), total=len(train_df)):\n        file_path = os.path.join(audio_dir, row['filename'])\n        try:\n            audio, sr = librosa.load(file_path, sr=32000)\n            rms_values = compute_rms_segments(audio, sr)\n            for i, rms in enumerate(rms_values):\n                rms_data.append([row['filename'], i, rms])\n        except Exception as e:\n            print(f\"Error processing {file_path}: {e}\")\n    rms_df = pd.DataFrame(rms_data, columns=['filename', 'segment_index', 'rms_value'])\n    rms_df.to_csv(output_csv, index=False)\n    print(f\"Saved RMS data to {output_csv}\")\n    return rms_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T18:32:30.778608Z","iopub.execute_input":"2025-05-09T18:32:30.779558Z","iopub.status.idle":"2025-05-09T18:32:30.790020Z","shell.execute_reply.started":"2025-05-09T18:32:30.779519Z","shell.execute_reply":"2025-05-09T18:32:30.788960Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 3. FocalBCELoss Implementation\nclass FocalBCELoss(nn.Module):\n    def __init__(self, alpha=0.25, gamma=2.0):\n        super(FocalBCELoss, self).__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n\n    def forward(self, inputs, targets):\n        bce_loss = nn.BCELoss(reduction='none')(inputs, targets)\n        pt = torch.exp(-bce_loss)\n        focal_loss = self.alpha * (1 - pt) ** self.gamma * bce_loss\n        return focal_loss.mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T18:33:15.418358Z","iopub.execute_input":"2025-05-09T18:33:15.418685Z","iopub.status.idle":"2025-05-09T18:33:15.425214Z","shell.execute_reply.started":"2025-05-09T18:33:15.418658Z","shell.execute_reply":"2025-05-09T18:33:15.423899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 4. Data Loading and RMS-based Filtering\nprint(\"Loading data...\")\ntrain_df = pd.read_csv('/kaggle/input/birdclef-2025/train.csv')\ntaxonomy_df = pd.read_csv('/kaggle/input/birdclef-2025/taxonomy.csv')\n\n# Precompute RMS if not exists\nrms_csv = 'train_segments.csv'\nif not os.path.exists(rms_csv):\n    rms_df = precompute_rms(train_df, '/kaggle/input/birdclef-2025/train_audio/', rms_csv)\nelse:\n    rms_df = pd.read_csv(rms_csv)\n\n# RMS-based filtering\nRMS_THRESHOLD = 0.01\nprint(\"Filtering segments with RMS >= \", RMS_THRESHOLD)\n# Keep segments with RMS >= threshold\nvalid_segments = rms_df[rms_df['rms_value'] >= RMS_THRESHOLD][['filename', 'segment_index']]\n\n# Identify rare species (<20 files)\nspecies_counts = train_df['primary_label'].value_counts()\nrare_species = species_counts[species_counts < 20].index\n\n# Keep all files for rare species\nrare_files = train_df[train_df['primary_label'].isin(rare_species)]['filename']\nrare_segments = rms_df[rms_df['filename'].isin(rare_files)][['filename', 'segment_index']]\n# For rare species, keep segment with highest RMS if no segment >= threshold\nfor filename in rare_files:\n    file_segments = rms_df[rms_df['filename'] == filename]\n    if not any(file_segments['rms_value'] >= RMS_THRESHOLD):\n        max_rms_segment = file_segments.loc[file_segments['rms_value'].idxmax()][['filename', 'segment_index']]\n        valid_segments = pd.concat([valid_segments, max_rms_segment.to_frame().T], ignore_index=True)\n\n# Downsampling\n# Keep rating=0 and rating>=3, limit 30 files per common species\nfiltered_df = train_df[train_df['filename'].isin(valid_segments['filename'])]\nfiltered_df = filtered_df[(filtered_df['rating'] == 0) | (filtered_df['rating'] >= 3) | (filtered_df['primary_label'].isin(rare_species))]\ncommon_species = species_counts[species_counts >= 20].index\ncommon_files = []\nfor species in common_species:\n    species_files = filtered_df[filtered_df['primary_label'] == species]['filename']\n    if len(species_files) > 30:\n        species_files = species_files.sample(n=30, random_state=42)\n    common_files.extend(species_files)\nfiltered_df = filtered_df[filtered_df['filename'].isin(common_files) | filtered_df['primary_label'].isin(rare_species)]\nprint(f\"Dataset size after RMS filtering and downsampling: {len(filtered_df)} files\")\n\n# Create segment mapping\nsegment_map = valid_segments.groupby('filename')['segment_index'].apply(list).to_dict()\n# For files with <3 valid segments, add one random segment\nfor filename in filtered_df['filename']:\n    valid_seg_indices = segment_map.get(filename, [])\n    if len(valid_seg_indices) < 3:\n        all_segments = rms_df[rms_df['filename'] == filename]['segment_index'].values\n        if len(all_segments) > 0:\n            random_seg = np.random.choice(all_segments)\n            if random_seg not in valid_seg_indices:\n                segment_map[filename].append(random_seg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T18:34:11.061153Z","iopub.execute_input":"2025-05-09T18:34:11.061456Z","iopub.status.idle":"2025-05-09T19:03:09.238283Z","shell.execute_reply.started":"2025-05-09T18:34:11.061435Z","shell.execute_reply":"2025-05-09T19:03:09.237235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 5. Audio Preprocessing\ndef preprocess_audio(audio, duration=5, sr=32000, n_mels=32):\n    \"\"\"Convert audio to normalized log-mel spectrogram.\"\"\"\n    target_length = sr * duration\n    if len(audio) < target_length:\n        audio = np.pad(audio, (0, target_length - len(audio)))\n    else:\n        audio = audio[:target_length]\n    mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr, n_mels=n_mels, fmax=16000)\n    log_mel_spec = librosa.power_to_db(mel_spec, ref=np.max)\n    log_mel_spec = (log_mel_spec - log_mel_spec.mean()) / (log_mel_spec.std() + 1e-8)\n    return log_mel_spec\n\ndef spec_augment(spec, time_mask=0.1, freq_mask=0.1):\n    \"\"\"Apply SpecAugment: time and frequency masking.\"\"\"\n    _, n_mels, n_steps = spec.shape\n    t_mask_size = int(n_steps * time_mask)\n    t_start = np.random.randint(0, n_steps - t_mask_size + 1)\n    spec[:, :, t_start:t_start + t_mask_size] = 0\n    f_mask_size = int(n_mels * freq_mask)\n    f_start = np.random.randint(0, n_mels - f_mask_size + 1)\n    spec[:, f_start:f_start + f_mask_size, :] = 0\n    return spec\n\ndef add_random_noise(audio, std=0.005):\n    \"\"\"Add Gaussian noise to audio.\"\"\"\n    noise = np.random.normal(0, std, audio.shape)\n    return audio + noise","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:10:52.435353Z","iopub.execute_input":"2025-05-09T19:10:52.435777Z","iopub.status.idle":"2025-05-09T19:10:52.447126Z","shell.execute_reply.started":"2025-05-09T19:10:52.435747Z","shell.execute_reply":"2025-05-09T19:10:52.445667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 6. Dataset Preparation\n# Create species list and mapping\nspecies_list = sorted(train_df['primary_label'].unique())\nspecies_to_idx = {s: i for i, s in enumerate(species_list)}\n\n# Create multi-label vectors\ndef create_multi_label_vector(row):\n    label_vec = np.zeros(len(species_list))\n    label_vec[species_to_idx[row['primary_label']]] = 1\n    for sec_label in eval(row['secondary_labels']):\n        if sec_label in species_to_idx:\n            label_vec[species_to_idx[sec_label]] = 1\n    return label_vec\n\nfiltered_df['multi_label'] = filtered_df.apply(create_multi_label_vector, axis=1)\n\n# Use all data for training (no validation split)\ntrain_files = filtered_df['filename']\ntrain_labels = np.stack(filtered_df['multi_label'].values)\n\n# Custom Dataset\nclass BirdCLEFDataset(Dataset):\n    def __init__(self, files, labels, audio_dir, segment_map, duration=5, sr=32000, n_mels=32, augment=False):\n        self.files = files\n        self.labels = labels\n        self.audio_dir = audio_dir\n        self.segment_map = segment_map\n        self.duration = duration\n        self.sr = sr\n        self.n_mels = n_mels\n        self.augment = augment\n\n    def __len__(self):\n        return len(self.files)\n\n    def __getitem__(self, idx):\n        filename = self.files.iloc[idx]\n        file_path = os.path.join(self.audio_dir, filename)\n        audio, _ = librosa.load(file_path, sr=self.sr)\n        # Select a valid segment\n        valid_segments = self.segment_map.get(filename, [0])\n        segment_idx = np.random.choice(valid_segments)\n        start_sample = segment_idx * self.duration * self.sr\n        audio_segment = audio[start_sample:start_sample + self.duration * self.sr]\n        if len(audio_segment) < self.duration * self.sr:\n            audio_segment = np.pad(audio_segment, (0, self.duration * self.sr - len(audio_segment)))\n        \n        if self.augment and np.random.rand() < 0.2:\n            audio_segment = add_random_noise(audio_segment, std=0.005)\n        \n        log_mel = preprocess_audio(audio_segment, self.duration, self.sr, self.n_mels)\n        log_mel = np.stack([log_mel] * 3, axis=0)  # Shape: (3, n_mels, time_steps)\n        \n        if self.augment:\n            log_mel = spec_augment(log_mel, time_mask=0.1, freq_mask=0.1)\n        \n        label = self.labels[idx]\n        return torch.tensor(log_mel, dtype=torch.float32), torch.tensor(label, dtype=torch.float32)\n\n# Create DataLoader\ntrain_dataset = BirdCLEFDataset(\n    train_files, train_labels, '/kaggle/input/birdclef-2025/train_audio/', segment_map, augment=True\n)\ntrain_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:11:45.292872Z","iopub.execute_input":"2025-05-09T19:11:45.293238Z","iopub.status.idle":"2025-05-09T19:11:45.422028Z","shell.execute_reply.started":"2025-05-09T19:11:45.293215Z","shell.execute_reply":"2025-05-09T19:11:45.420997Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 7. Model Architecture\nclass EfficientNetB0(nn.Module):\n    def __init__(self, num_classes=len(species_list)):\n        super(EfficientNetB0, self).__init__()\n        self.efficientnet = efficientnet_b0(weights=None)\n        # Load pretrained weights offline\n        self.efficientnet.load_state_dict(\n            torch.load('/kaggle/input/efficientnet-b0/efficientnet_b0_rwightman-7f5810bc.pth')\n        )\n        # Replace classifier for multi-label classification\n        in_features = self.efficientnet.classifier[1].in_features\n        self.efficientnet.classifier = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(in_features, 128),  # Reduced to 128 for speed\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes),\n            nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        return self.efficientnet(x)\n\nmodel = EfficientNetB0().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:12:14.241186Z","iopub.execute_input":"2025-05-09T19:12:14.241527Z","iopub.status.idle":"2025-05-09T19:12:14.769431Z","shell.execute_reply.started":"2025-05-09T19:12:14.241499Z","shell.execute_reply":"2025-05-09T19:12:14.768444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 8. Model Training\nprint(\"Training model...\")\ncriterion = FocalBCELoss(alpha=0.25, gamma=2.0)\noptimizer = optim.Adam(model.parameters(), lr=5e-3)\nscheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)\n\ndef train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    total_loss = 0\n    for batch_idx, (inputs, targets) in enumerate(tqdm(loader, desc=\"Training\")):\n        inputs, targets = inputs.to(device), targets.to(device)\n        # Mixup\n        if np.random.rand() < 0.3:  # Reduced to 30%\n            alpha = 0.2\n            lam = np.random.beta(alpha, alpha)\n            perm = torch.randperm(inputs.size(0))\n            mixed_inputs = lam * inputs + (1 - lam) * inputs[perm]\n            mixed_targets = lam * targets + (1 - lam) * targets[perm]\n            optimizer.zero_grad()\n            outputs = model(mixed_inputs)\n            loss = criterion(outputs, mixed_targets)\n            loss.backward()\n            optimizer.step()\n            total_loss += loss.item()\n        else:\n            optimizer.zero_grad()\n            outputs = model(inputs)\n            loss = criterion(outputs, targets)\n            loss.backward()\n            optimizer.step()\n            total_loss += loss.item()\n    return total_loss / len(loader)\n\n# Training loop with Early Stopping\nbest_train_loss = float('inf')\npatience = 2\nepochs_without_improvement = 0\nfor epoch in range(3):  # Max 3 epochs\n    train_loss = train_one_epoch(model, train_loader, criterion, optimizer)\n    print(f\"Epoch {epoch+1}, Train Loss: {train_loss:.4f}\")\n    \n    # Save model if train loss improves\n    if train_loss < best_train_loss:\n        best_train_loss = train_loss\n        torch.save(model.state_dict(), 'best_model.pth')\n        epochs_without_improvement = 0\n    else:\n        epochs_without_improvement += 1\n    \n    # Update learning rate\n    scheduler.step(train_loss)\n    \n    # Early stopping\n    if epochs_without_improvement >= patience:\n        print(f\"Early stopping triggered after {epoch+1} epochs\")\n        break\n\n# Load best model\nmodel.load_state_dict(torch.load('best_model.pth'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:12:42.676936Z","iopub.execute_input":"2025-05-09T19:12:42.677253Z","iopub.status.idle":"2025-05-09T19:25:43.487186Z","shell.execute_reply.started":"2025-05-09T19:12:42.677230Z","shell.execute_reply":"2025-05-09T19:25:43.486030Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 9. Inference on Test Soundscapes\ndef predict_test_soundscapes(model, test_dir):\n    model.eval()\n    submission = []\n    test_files = glob.glob(f'{test_dir}/*.ogg')\n    \n    if not test_files:\n        print(\"No test soundscape files found. Returning empty submission.\")\n        columns = ['row_id'] + species_list\n        return pd.DataFrame([], columns=columns)\n    \n    for file in test_files:\n        audio, sr = librosa.load(file, sr=32000)\n        soundscape_id = os.path.basename(file).replace('.ogg', '')\n        \n        for i in range(0, len(audio), 5*sr):\n            end_time = (i // sr) + 5\n            segment = audio[i:i+5*sr]\n            if len(segment) < 5*sr:\n                segment = np.pad(segment, (0, 5*sr - len(segment)))\n            \n            log_mel = preprocess_audio(segment, n_mels=32)\n            log_mel = np.stack([log_mel] * 3, axis=0)  # Shape: (3, n_mels, time_steps)\n            log_mel = torch.tensor(log_mel, dtype=torch.float32).unsqueeze(0).to(device)\n            \n            with torch.no_grad():\n                pred = model(log_mel).cpu().numpy()[0]\n            \n            row_id = f\"{soundscape_id}_{end_time}\"\n            submission.append([row_id] + pred.tolist())\n    \n    columns = ['row_id'] + species_list\n    submission_df = pd.DataFrame(submission, columns=columns)\n    return submission_df\n\nprint(\"Generating predictions...\")\ntest_dir = '/kaggle/input/birdclef-2025/test_soundscapes'\nsubmission_df = predict_test_soundscapes(model, test_dir)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:26:53.330422Z","iopub.execute_input":"2025-05-09T19:26:53.330836Z","iopub.status.idle":"2025-05-09T19:26:53.353672Z","shell.execute_reply.started":"2025-05-09T19:26:53.330804Z","shell.execute_reply":"2025-05-09T19:26:53.352540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 10. Submission File Validation\nprint(\"Validating submission...\")\nsample_submission = pd.read_csv('/kaggle/input/birdclef-2025/sample_submission.csv')\nassert set(submission_df.columns) == set(sample_submission.columns), \"Column mismatch\"\nif submission_df.shape[0] > 0:\n    assert submission_df.iloc[:, 1:].ge(0).all().all() and submission_df.iloc[:, 1:].le(1).all().all(), \"Invalid probabilities\"\nsubmission_df.to_csv('submission.csv', index=False)\nprint(\"Submission file saved as submission.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-09T19:27:17.590152Z","iopub.execute_input":"2025-05-09T19:27:17.590587Z","iopub.status.idle":"2025-05-09T19:27:17.612990Z","shell.execute_reply.started":"2025-05-09T19:27:17.590558Z","shell.execute_reply":"2025-05-09T19:27:17.611692Z"}},"outputs":[],"execution_count":null}]}