{"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":25954,"databundleVersionId":2091745,"sourceType":"competition"},{"sourceId":11498503,"sourceType":"datasetVersion","datasetId":7201120}],"dockerImageVersionId":31012,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import timm\nimport torch \nimport torch.nn as nn \nimport torch.nn.functional as F\nimport torchvision.transforms as T\nfrom torch.utils.data import DataLoader\nimport numpy as np\nimport random\nimport os\nimport librosa as lb\nimport librosa.display as lbd\nimport math \nimport pandas as pd \nfrom pathlib import Path\nfrom torch.utils.data import Dataset","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.179455Z","iopub.execute_input":"2025-04-23T13:02:33.179864Z","iopub.status.idle":"2025-04-23T13:02:33.188057Z","shell.execute_reply.started":"2025-04-23T13:02:33.179836Z","shell.execute_reply":"2025-04-23T13:02:33.186921Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(seed=42):\n    \"\"\"Set seed for reproducibility and testing.\"\"\"\n    random.seed(seed)  # Python random seed\n    np.random.seed(seed)  # Numpy random seed\n    torch.manual_seed(seed)  # PyTorch CPU seed\n    torch.cuda.manual_seed(seed)  # PyTorch GPU seed\n    torch.cuda.manual_seed_all(seed)  # Multi-GPU seed\n    torch.backends.cudnn.deterministic = True  # Ensure deterministic behavior\n    torch.backends.cudnn.benchmark = False  # Disable optimization for non-deterministic behavior\n\nset_seed()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.190078Z","iopub.execute_input":"2025-04-23T13:02:33.19048Z","iopub.status.idle":"2025-04-23T13:02:33.213979Z","shell.execute_reply.started":"2025-04-23T13:02:33.190454Z","shell.execute_reply":"2025-04-23T13:02:33.212768Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"base_path = \"/kaggle/input/birdclef-2021/train_short_audio\"\nbird_dirs = sorted(os.listdir(base_path))\n\nBIRDS_TO_LABELS = {bird: idx for idx, bird in enumerate(bird_dirs)}\nLABELS_TO_BIRDS = {idx: bird for bird, idx in BIRDS_TO_LABELS.items()}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.215329Z","iopub.execute_input":"2025-04-23T13:02:33.215617Z","iopub.status.idle":"2025-04-23T13:02:33.240738Z","shell.execute_reply.started":"2025-04-23T13:02:33.215598Z","shell.execute_reply":"2025-04-23T13:02:33.23962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_CLASSES = 387 \n\nclass  Model(nn.Module):\n    def __init__(self, num_classes=NUM_CLASSES):\n        super(Model, self).__init__()\n\n        self.num_classes = num_classes\n        \n        # TODO: Load pre-trained full model here (efficientnet + fc layer trained on the training data)\n        \n        self._model = timm.create_model('efficientnet_b0', pretrained=True)\n        \n        # Freeze the base model (optional) - we only train the last MLP layers for new samples \n        for param in self._model.parameters():\n            param.requires_grad = False\n        \n        # Replace the final classifier layer with one for multi-label classification\n        # NOTE: These params are not frozen \n        self._model.classifier = nn.Sequential(\n            nn.Linear(in_features=self._model.classifier.in_features, out_features=1024),  # First hidden layer\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(1024, 512),  # Second hidden layer\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(512, self.num_classes)  # Output layer (387 classes)\n        )\n\n    def forward(self, x):\n        return self._model(x)\n\n    def get_uncertainty(self, probs): \n        # Approach 1: BALD \n        probs_mean = probs.mean(dim=0)  # [batch_size, num_classes]\n        predictive_entropy = -torch.sum(probs_mean * torch.log(probs_mean + 1e-8), dim=-1)  # [batch_size]\n\n        entropy_per_sample = -torch.sum(probs * torch.log(probs + 1e-8), dim=-1)  # [N, batch_size]\n        model_entropy = entropy_per_sample.mean(dim=0) # [batch_size]\n    \n        mutual_information = predictive_entropy - model_entropy \n\n        # TODO (?): Other approaches (e.g. first min over the num_preds, then total entropy), total_entropy with just 1 pred\n        return mutual_information \n\n    def inference(self, x, num_preds=10):\n        \"\"\"Inference function that returns probabilities and uncertainty.\"\"\"\n        # Assumes x has shape (batch_size, channels, height, width) (even for batch size 1)\n        \n        # Set to train so droupout still works (so we get random outputs)\n        self.train() \n\n        # Pass the input through the EfficientNet feature extractor once (for efficiency), not the classification head \n        features = self._model.forward_features(x)\n        pooled = self._model.global_pool(features) # (batch_size, classifier_in)\n\n        # Run through classifier head N times \n        pooled_repeated = pooled.unsqueeze(0).repeat(num_preds, 1, 1)  # (num_samples, batch_size, classifier_in)\n        pooled_repeated = pooled_repeated.view(-1, *pooled.size()[1:])  # Flatten to (num_samples * batch_size, ...)\n        logits = self._model.classifier(pooled_repeated)\n        probs = torch.sigmoid(logits) \n        probs = probs.view(num_preds, -1, self.num_classes) # Reshape back to (num_samples, batch_size, num_classes)\n\n        assert probs.shape[0] == num_preds\n        assert probs.shape[1] == x.shape[0] # batch_size \n        assert probs.shape[2] == self.num_classes \n        \n        # For final prediction, take min/mean across all dimensions (min to reduce uncertainty) - but maybe don't even need this\n        # TODO (possibly): already introduce thresholding here for outputting just bird class labels \n        return probs.mean(dim=0), self.get_uncertainty(probs)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.242403Z","iopub.execute_input":"2025-04-23T13:02:33.24279Z","iopub.status.idle":"2025-04-23T13:02:33.25584Z","shell.execute_reply.started":"2025-04-23T13:02:33.24276Z","shell.execute_reply":"2025-04-23T13:02:33.25469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Instantiate the model\n# model = Model(num_classes=387)\n\n# # Set the model to evaluation mode\n# model.eval()\n\n# # Create a random input batch (batch_size, channels, height, width)\n# batch_size = 8  # Example batch size\n# random_input = torch.randn(batch_size, 3, 224, 224)  # Random tensor with the shape of an image batch\n\n# # Run inference\n# with torch.no_grad():  # No gradients needed during inference\n#     probabilities, uncertainty = model.inference(random_input)\n\n# # Print the outputs\n# print(\"Probabilities shape:\", probabilities.shape)  # Shape should be (batch_size, num_classes)\n# print(\"Uncertainty shape:\", uncertainty.shape, \" Uncertainties: \", uncertainty)  # Shape should be (batch_size, 1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:07:24.182795Z","iopub.execute_input":"2025-04-23T13:07:24.183116Z","iopub.status.idle":"2025-04-23T13:07:24.188217Z","shell.execute_reply.started":"2025-04-23T13:07:24.183093Z","shell.execute_reply":"2025-04-23T13:07:24.187313Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Example\nHere, we \n- Load a new long form soundscape\n- Break it up into 5s segments, conver them to spectograms, convert each spectogram to the correct shape for EfficientNet\n- Do inference over all segments, outputting probs and uncertainty for each\n- Suggest the K input segments with the highest uncertainty (OR later: output all input segments with uncertainty above a certain threshold or something)\n\nThen, we \n- Get labels for the K input segments\n- Retrain the MLP using the entire dataset (TODO: fine-tune with just these K input segments?) \n- Save the new model \n- (?) Evaluate the new model's performance \n\nThis can be repeated indefinitely","metadata":{}},{"cell_type":"code","source":"# Spectogram generation and conversion\ndef cyclic_pad(y, length): \n    n_repeats = length // len(y)\n    epsilon = length % len(y)\n    y = np.concatenate([y]*n_repeats + [y[:epsilon]])\n    return y \n\ndef get_log_spectogram(y, sr):\n    # Got features from another Kaggle notebook, see spectogram generation \n    melspec = lb.feature.melspectrogram(\n            y=y, sr=sr, n_fft = 1024, hop_length = 500, n_mels = 128, fmin = 40, fmax = 15000, power = 2\n        )\n\n    # Convert to log-scale \n    log_melspec = lb.power_to_db(melspec).astype(np.float32)\n\n    return log_melspec \n\n\n# Return a tensor with dimension [num_spectograms, spectogram_width, spectogram_height]\ndef get_spectograms(audio_file, segment_duration): \n    y, sr = lb.load(audio_file, sr=None) \n\n    # Calculate the number of samples in a 5-second segment\n    segment_length = segment_duration * sr\n    num_segments = math.ceil(len(y) / segment_length)\n\n    transform = T.Compose([\n            T.ToTensor(),  # from numpy HxW to tensor [1, H, W]\n            T.Resize((224, 224)),\n            T.Lambda(lambda x: x.repeat(3, 1, 1)),  # [1, H, W] → [3, H, W]\n            T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # TODO: ASSUMES PRE-TRINAED IMG NET, SHOULD COMPUTE OVER OWN DATASET \n        ])\n    \n    spectograms = []\n    \n    for i in range(num_segments):\n        start_idx = i * segment_length\n        end_idx = min(start_idx + segment_length, len(y))\n        segment = y[start_idx:end_idx]\n\n        if len(segment) < segment_length: \n            segment = cyclic_pad(segment, segment_length)\n        \n        # Generate the spectrogram for this segment\n\n        log_spectrogram = get_log_spectogram(segment, sr) # Shape generated by librosa: (128, 321)\n        log_spectrogram_img = transform(log_spectrogram)  # Convert to correct shape for EfficientNet: [3, 224, 224]\n        \n        spectograms.append(log_spectrogram_img)\n\n    return torch.stack(spectograms) ","metadata":{"trusted":true,"_kg_hide-input":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.98834Z","iopub.execute_input":"2025-04-23T13:02:33.988633Z","iopub.status.idle":"2025-04-23T13:02:33.998581Z","shell.execute_reply.started":"2025-04-23T13:02:33.988611Z","shell.execute_reply":"2025-04-23T13:02:33.997439Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load new audio file and get spectograms\nDURATION = 5 # 5 seconds per segment \nfile = Path('/kaggle/input/birdclef-2021/train_soundscapes/10534_SSW_20170429.ogg')\nspecs = get_spectograms(file, segment_duration=DURATION)\nprint(specs.shape)\n\nlbd.specshow(specs[1, 0].numpy(), cmap=\"magma\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:33.99994Z","iopub.execute_input":"2025-04-23T13:02:34.000259Z","iopub.status.idle":"2025-04-23T13:02:36.198508Z","shell.execute_reply.started":"2025-04-23T13:02:34.000236Z","shell.execute_reply":"2025-04-23T13:02:36.197413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# probs, uncertainties = model.inference(specs)\n# print(probs.shape, uncertainties.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:36.19963Z","iopub.execute_input":"2025-04-23T13:02:36.200042Z","iopub.status.idle":"2025-04-23T13:02:48.567206Z","shell.execute_reply.started":"2025-04-23T13:02:36.200006Z","shell.execute_reply":"2025-04-23T13:02:48.566114Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Send these uncertainty values to the frontend **\n- It visualizes them over the audio\n- It makes the user label the most uncertain segments\n- Then, it sends back the labelled segments","metadata":{}},{"cell_type":"code","source":"# K = 10 \n# top_k_values,top_k_indices = torch.topk(uncertainties, K, largest=True)\n\n# # Print the results\n# print(\"Top 10 Uncertainties:\", top_k_values)\n# print(\"Corresponding Segment Indices:\", top_k_indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:48.568326Z","iopub.execute_input":"2025-04-23T13:02:48.568624Z","iopub.status.idle":"2025-04-23T13:02:48.57696Z","shell.execute_reply.started":"2025-04-23T13:02:48.5686Z","shell.execute_reply":"2025-04-23T13:02:48.575909Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Now, backend will use these new labels to retrain the model**\n- After this, it can recompute the uncertainty scores for the same segment, for example\n- We can stop when uncertainty is below a certain threshold ","metadata":{}},{"cell_type":"markdown","source":"Now, we retrain the model with the new (and old) labels ","metadata":{}},{"cell_type":"code","source":"class BirdSpectrogramDataset(Dataset):\n    def __init__(self, specs=None, labels=None):\n        # TODO: In real use case, would just store a filepath or something to the specs, then load the file in __getitem__ \n        # Assume this would already be filled with old data \n        self.samples = specs if specs else []\n        self.labels = labels if labels else []\n    \n    def __len__(self):\n        return len(self.samples)\n    \n    def __getitem__(self, idx):\n        \n        # TODO: In real scenario \n        # path, bird_name = self.samples[idx]\n        # spec_stack = np.load(path)  # [12, 128, 321]\n        # Select a random 5s slice\n        #slice_idx = random.randint(0, spec_stack.shape[0] - 1)\n        #spec = spec_stack[slice_idx]  # shape [128, 321]\n        # Normalize and convert to 3-channel for EfficientNet\n        # spec = torch.tensor(spec).float().unsqueeze(0)  # [1, 128, 321]\n        # spec = spec.repeat(3, 1, 1)  # [3, 128, 321]\n        \n        return self.samples[idx], self.labels[idx]\n\n    def add_sample(self, spec, label): \n        # spec is already a tensor\n        self.samples.append(spec)\n\n        label_tensor = torch.zeros(NUM_CLASSES, dtype=torch.float)\n        \n        # Set indices corresponding to the present bird species to 1\n        for bird in label:\n            label_tensor[BIRDS_TO_LABELS[bird]] = 1  # Mark it as present\n            \n        self.labels.append(label_tensor)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:48.596394Z","iopub.execute_input":"2025-04-23T13:02:48.596745Z","iopub.status.idle":"2025-04-23T13:02:48.613962Z","shell.execute_reply.started":"2025-04-23T13:02:48.596717Z","shell.execute_reply":"2025-04-23T13:02:48.612973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# For now, we retrain on the entire dataset\ndef retrain_model(model, dataset, num_epochs=10, batch_size=32, lr=1e-3):\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n    print(f\"Retraining... Using {len(dataset)} items\")\n    dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)\n    model.to(device)\n\n    optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=lr)\n    criterion = torch.nn.BCEWithLogitsLoss()  # for multi-label classification\n\n    model.train()\n    for epoch in range(num_epochs):\n        total_loss = 0.0\n        for specs, labels in dataloader:\n            specs, labels = specs.to(device), labels.to(device)\n\n            # Forward\n            outputs = model(specs)\n            loss = criterion(outputs, labels)\n\n            # Backward\n            optimizer.zero_grad()\n            loss.backward()\n            optimizer.step()\n\n            total_loss += loss.item()\n\n        print(f\"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss / len(dataloader):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:02:48.615507Z","iopub.execute_input":"2025-04-23T13:02:48.615857Z","iopub.status.idle":"2025-04-23T13:02:48.639649Z","shell.execute_reply.started":"2025-04-23T13:02:48.615829Z","shell.execute_reply":"2025-04-23T13:02:48.638492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# USUALLY FRONTEND USER WOULD SEND THIS, NOW USE GROUND TRUTH \ndef get_labels(file, idx): \n    df = pd.read_csv('/kaggle/input/birdclef-2021/train_soundscape_labels.csv') \n    rid = file.name.split(\"_\")[0] + '_' + file.name.split(\"_\")[1] + '_' + str((idx+1) * 5)\n    birds = df[df['row_id'] == rid].birds.item()\n    if birds == 'nocall':\n        labels = []\n    else: \n        labels = [bird for bird in birds.split(' ')]\n    return labels\n\n    \n\ndf = pd.read_csv('/kaggle/input/birdclef-2021/train_soundscape_labels.csv') \nground_truth = []\n\nfor idx in range(120):\n    labels = get_labels(file, idx)\n\n    label_tensor = torch.zeros(NUM_CLASSES, dtype=torch.float)\n        \n    # Set indices corresponding to the present bird species to 1\n    for bird in labels:\n        label_tensor[BIRDS_TO_LABELS[bird]] = 1  # Mark it as present\n    \n    ground_truth.append(label_tensor)\n\n\nground_truth = torch.stack(ground_truth)\nground_truth.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:05:50.163226Z","iopub.execute_input":"2025-04-23T13:05:50.163633Z","iopub.status.idle":"2025-04-23T13:05:50.941475Z","shell.execute_reply.started":"2025-04-23T13:05:50.163608Z","shell.execute_reply":"2025-04-23T13:05:50.940369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ground truth labels for comparison \n\n\ndef evaluate_model(probs, labels, threshold=0.5):\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    correct = 0.0\n    total = 0.0\n\n    # for i in range(20):\n    #     label_row = labels[i]\n    #     prob_row = probs[i]\n    \n    #     # Get indices where label is 1\n    #     positive_indices = (label_row == 1).nonzero(as_tuple=True)[0]\n    \n    #     print(f\"Sample {i}:\")\n        \n    #     for idx in positive_indices:\n    #         idx = idx.item()\n    #         print(f\"  ✓ Label {idx} -> Prob: {prob_row[idx].item():.4f}\")\n        \n    #     # Always show label 0\n    #     print(f\"  [Always show] Label 0 -> Prob: {prob_row[0].item():.4f}\")\n    #     print(\"------\")\n\n\n    with torch.no_grad():\n        probs = probs.to(device)\n        labels = labels.to(device)\n\n        \n        print()\n\n        # 1 if sigmoid > 0.5 for that bird, else 0 \n        preds = (probs > threshold).float()\n\n        print(preds.shape)\n\n        # Partial credit: intersection / max(true, pred)\n        # intersection = (preds * labels).sum(dim=1)\n        # denom = torch.max(preds.sum(dim=1), labels.sum(dim=1)).clamp(min=1)\n        # scores = intersection / denom\n        # total_score += scores.sum().item()\n        # total_samples += labels.size(0)\n\n        matches = (preds == labels).all(dim=1)\n        correct += matches.sum().item()\n        total += labels.size(0)\n\n    accuracy = correct / total\n    print(f\"Evaluation Score: {accuracy:.4f} (accuracy)\")\n    return accuracy\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:07:40.395826Z","iopub.execute_input":"2025-04-23T13:07:40.396178Z","iopub.status.idle":"2025-04-23T13:07:40.4046Z","shell.execute_reply.started":"2025-04-23T13:07:40.396153Z","shell.execute_reply":"2025-04-23T13:07:40.4034Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Instantiate the model\nmodel = Model(num_classes=387)\ndataset = BirdSpectrogramDataset()\n\n\nlabels_queue = {}\n\n# This happens until the whole file is above a certain uncertainty / the whole file is labelled\nfor j in range(3):\n    # 1) (Backend): Get current uncertainties and send to frontend \n    probs, uncertainties = model.inference(specs)\n\n    # 2) (Frontend): Get the most uncertain segments, display uncertainty for each segment \n    K = 4\n    top_k_values,top_k_indices = torch.topk(uncertainties, K, largest=True)\n    print('Most uncertain segments: ' , top_k_indices)\n    print('Uncertainty values: ', top_k_values)\n\n    # Label segments, one at a time (user could keep labelling while retraining)\n    for i in range(4): \n        labels_queue[top_k_indices[i].item()]= get_labels(file, top_k_indices[i].item())\n\n    # 3): (Backend): Retrain once we reach a certain number of labelled samples\n    # THIS WOULD HAPPEN ON BACKEND (WHERE LABEL QUEUE IS FILLED)\n    if len(labels_queue.keys()) == 4: \n        for idx, label in labels_queue.items():\n            dataset.add_sample(specs[idx], label)\n            \n        # RESET QUEUE \n        labels_queue = {} \n        # Evaluate model before \n        evaluate_model(probs, ground_truth)\n        retrain_model(model, dataset)\n        new_probs, _ = model.inference(specs)\n        # Evaluate model after\n        evaluate_model(new_probs, ground_truth)\n\n        # Reset model (?)\n        model = Model(num_classes=387)\n    print(\"Loop done\")\n    print()\n    print()\n\n\nprint(f'Still {len(labels_queue)} items in labels queue')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:07:42.995648Z","iopub.execute_input":"2025-04-23T13:07:42.995949Z","iopub.status.idle":"2025-04-23T13:09:12.323754Z","shell.execute_reply.started":"2025-04-23T13:07:42.995931Z","shell.execute_reply":"2025-04-23T13:09:12.322647Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Baseline model** ","metadata":{}},{"cell_type":"code","source":"df = pd.read_csv('/kaggle/input/birdclef-2021/train_soundscape_labels.csv') \ndf_file = df[df['row_id'].str.startswith(file.name.split(\"_\")[0] + '_' + file.name.split(\"_\")[1])]\nprint(\"Baseline accuracy (only nocall predict: \", len(df_file[df_file['birds']=='nocall']) / len(df_file))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:12:21.297764Z","iopub.execute_input":"2025-04-23T13:12:21.298075Z","iopub.status.idle":"2025-04-23T13:12:21.314654Z","shell.execute_reply.started":"2025-04-23T13:12:21.298046Z","shell.execute_reply":"2025-04-23T13:12:21.313751Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Analyzing the predictions: all birds where the model predicts something else than nocall","metadata":{}},{"cell_type":"code","source":"for i in range(new_probs.shape[0]):\n    sample_probs = new_probs[i]\n    sample_gt = ground_truth[i]\n\n    if (sample_probs < 0.5).all():  # It's a nocall → skip\n        continue\n\n    pred_indices = (sample_probs > 0.5).nonzero(as_tuple=True)[0].tolist()\n    gt_indices = (sample_gt == 1).nonzero(as_tuple=True)[0].tolist()\n\n    pred_birds = [LABELS_TO_BIRDS[idx] for idx in pred_indices]\n    gt_birds = [LABELS_TO_BIRDS[idx] for idx in gt_indices]\n\n    print(f\"Sample {i}:\")\n    print(f\"  ✅ Predicted birds: {pred_birds}\")\n    print(f\"  🎯 Ground truth birds: {gt_birds}\")\n    print(\"------\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-23T13:15:26.285987Z","iopub.execute_input":"2025-04-23T13:15:26.286422Z","iopub.status.idle":"2025-04-23T13:15:26.303529Z","shell.execute_reply.started":"2025-04-23T13:15:26.286396Z","shell.execute_reply":"2025-04-23T13:15:26.302038Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}