{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.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":91844,"databundleVersionId":11361821,"sourceType":"competition"},{"sourceId":11053663,"sourceType":"datasetVersion","datasetId":6886569},{"sourceId":11547862,"sourceType":"datasetVersion","datasetId":7185068}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Pipeline de treinamento \n#### Realizada através do estudo do [notebook](https://www.kaggle.com/code/kadircandrisolu/efficientnet-b0-pytorch-train-birdclef-25) do [Kadircan İdrisoğlu](https://www.kaggle.com/kadircandrisolu). Grande parte desse notebook foi retirado ou versionado do Notebook dele.\n\nTentei fazer uma pipeline inicial bem crua, com o mínimo de pré-processamento e sem nada escolhido a dedo (com excessão de alguns parâmetros dos espectrogramas e outras configurações da classe de config, esses valores foram retirados dos ganhadores das competições passadas e do notebook do Kadircan). Não tem nenhum data augmentation, sem normalização, scheduler, early stropping, etc.","metadata":{}},{"cell_type":"code","source":"%matplotlib inline\n\nimport os\nimport random\nimport time\nimport math\n\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.optim import lr_scheduler\nfrom torch.utils.data import Dataset, DataLoader, Subset\n\nfrom tqdm.auto import tqdm\nimport timm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.151714Z","iopub.execute_input":"2025-04-27T20:00:52.152050Z","iopub.status.idle":"2025-04-27T20:00:52.159020Z","shell.execute_reply.started":"2025-04-27T20:00:52.152024Z","shell.execute_reply":"2025-04-27T20:00:52.158060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class cfg:\n    \n    output_dir = '/kaggle/working/'\n    train_datadir = '/kaggle/input/birdclef-2025/train_audio'\n    train_csv = '/kaggle/input/birdclef-2025/train.csv'\n    test_soundscapes = '/kaggle/input/birdclef-2025/test_soundscapes'\n    submission_csv = '/kaggle/input/birdclef-2025/sample_submission.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n\n\n    SEED = 42\n    debug_on = True\n    is_files_loaded = True\n    loaded_specs = None\n    \n    \n    SR = 32000\n    TARGET_SHAPE = (256, 256)\n    TARGET_DURATION = 5.0\n    N_FFT = 1024\n    HOP_LENGTH = 500\n    N_MELS = 128\n    FMIN = 40\n    FMAX = 15000\n    POWER = 2\n    is_normalized = False\n    \n                        \n    model_name = 'efficientnet_b0'  \n    is_pre_trained = True\n    input_channels = 1\n    \n    optimizer = 'AdamW'\n    lr = 5e-4 \n    weight_decay = 1e-5\n    epochs = 10  \n    batch_size = 32  \n    criterion = 'BCEWithLogitsLoss'\n    n_folds = 5\n\n\n    device = 'cpu'\n    gpu_on = True\n    if gpu_on:\n        device = 'cuda' if torch.cuda.is_available() else 'cpu'\n        \n    if debug_on:\n        epochs = 2\n\n    def load_spectrograms(self):\n        if self.is_files_loaded:\n            loaded_specs = '/kaggle/input/birdclef25-mel-spectrograms/birdclef2025_melspec_5sec_256_256.npy'\n            return np.load(loaded_specs, allow_pickle=True).item()\n        \ncfg = cfg()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.165830Z","iopub.execute_input":"2025-04-27T20:00:52.166169Z","iopub.status.idle":"2025-04-27T20:00:52.181341Z","shell.execute_reply.started":"2025-04-27T20:00:52.166146Z","shell.execute_reply":"2025-04-27T20:00:52.180698Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Retirada do Notebook do Kadircan İdrisoğlu\ndef set_seed(seed=42):\n    \"\"\"\n    Set seed for reproducibility\n    \"\"\"\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.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nset_seed(cfg.SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:20:19.080026Z","iopub.execute_input":"2025-04-27T20:20:19.080344Z","iopub.status.idle":"2025-04-27T20:20:19.138630Z","shell.execute_reply.started":"2025-04-27T20:20:19.080307Z","shell.execute_reply":"2025-04-27T20:20:19.137725Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- Taxonomy:\n  - Dataframe contendo as informações de cada espécie\n- Train:\n  - Dataframe de treino, contém a label (primary_label) de cada sample (filename), que são os dados que nós passamos para o modelo\n- Spectrograms:\n  - Dicionário que contém os espectrogramas de cada um dos arquivos de treino, escolha is_file_loaded = True para pré-carregar os dados, caso contrário os espectrogramas serão gerados na construção do Dataset","metadata":{}},{"cell_type":"code","source":"taxonomy = pd.read_csv(cfg.taxonomy_csv)\ndf = pd.read_csv(cfg.train_csv)\nspectrograms = cfg.load_spectrograms()\n\ndf['class'] = df['primary_label'].map(taxonomy.set_index('primary_label')['class_name'])\ndf.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.213752Z","iopub.status.idle":"2025-04-27T20:00:52.214034Z","shell.execute_reply":"2025-04-27T20:00:52.213923Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Funções para acessar um arquivo aleatório e para visualização\n\n#Atenção, código mais feio do mundo abaixo\ndef random_file(idx=None, class_name=None):\n    if idx is not None and class_name is None:\n        return df['filename'][idx]\n    if class_name is not None and idx is None:\n        aux = df.loc[df['class'] == class_name]\n        auxIndex = df.loc[df['class'] == class_name].index.tolist()\n        return aux.iloc[auxIndex[random.randint(0, len(aux)-1)]]['filename']\n    elif class_name is not None and idx is not None:\n        aux = df.loc[df['class'] == class_name]\n        auxIndex = df.loc[df['class'] == class_name].index.tolist()\n        return aux.iloc[auxIndex[idx]]['filename']\n    return df['filename'][random.randint(0, len(df)-1)]\n\ndef visualize_melspec(spec, cfg):\n    fig, ax = plt.subplots()\n\n    img = librosa.display.specshow(spec, x_axis='time',\n                             y_axis='mel', sr=cfg.SR,\n                             fmax=cfg.FMAX, ax=ax)\n    fig.colorbar(img, ax=ax, format='%+2.0f dB')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.214876Z","iopub.status.idle":"2025-04-27T20:00:52.215259Z","shell.execute_reply":"2025-04-27T20:00:52.215090Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- audio_to_melspec:\n  - Função que transofrma um array de um áudio em um Mel-spectrogram\n- process_audio_file:\n  - Corta o arquivo de áudio na em determinada faixa e aplica a função audio_to_melspec, além de outras transformações\n- generate_spectrograms:\n  - Função que cria e salva o dicionário com os espectrogramas de cada arquivo de áudio","metadata":{}},{"cell_type":"code","source":"def audio_to_melspec(data: np.ndarray, cfg: object) -> np.ndarray:\n    mel_spec = librosa.feature.melspectrogram(\n            y=data,\n            sr=cfg.SR,\n            n_fft=cfg.N_FFT,\n            hop_length=cfg.HOP_LENGTH,\n            n_mels=cfg.N_MELS,\n            fmin=cfg.FMIN,\n            fmax=cfg.FMAX,\n            power=cfg.POWER\n        )\n    \n    mel_spec = librosa.power_to_db(mel_spec, ref=np.max)\n    \n    if cfg.is_normalized:\n        mel_spec = (mel_spec - mel_spec.min()) / (mel_spec.max() - mel_spec.min() + 1e-8)\n        return mel_spec_norm\n        \n    return mel_spec\n\ndef process_audio_file(file: str, cfg: object) -> np.ndarray:\n    \n    audio, _ = librosa.load(os.path.join(cfg.train_datadir, file))\n    target_samples = int(cfg.TARGET_DURATION * cfg.SR)\n    \n    if len(audio) < target_samples:                            # áudio pequeno\n        n_copy = math.ceil(target_samples / len(audio))\n        if n_copy > 1:\n            audio = np.concatenate([audio] * n_copy)           # duplica o áudio\n            \n    start_idx = max(0, int(len(audio) / 2 - target_samples / 2))\n    end_idx = min(len(audio), start_idx + target_samples)\n    center_audio = audio[start_idx:end_idx]\n    \n    mel_spec = audio_to_melspec(center_audio, cfg)                # cria o mel spectrograma\n    \n    if len(center_audio) < target_samples:\n        center_audio = np.pad(center_audio, \n                             (0, target_samples - len(center_audio)), \n                             mode='constant')\n    \n    if mel_spec.shape != cfg.TARGET_SHAPE:                      # resize pro input da cnn\n        mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n    \n    return mel_spec\n\ndef generate_spectrograms(df: pd.DataFrame, cfg: object, name: str = 'spectrograms', save = False) -> dict:\n    \n    \"\"\"\n    df: dataframe de treino\n    cfg: classe de configuração\n    name: nome do arquivo ao salvar\n    save: se True, salva o dicionário em '/kaggle/working/name.npy'.\n        Lembre-se de baixar o arquivo antes de finalizar a sessão do Kaggle.\n    \"\"\"\n    print(\"Generating mel spectrograms from audio files...\")\n    start_time = time.time()\n\n    all_bird_data = {}\n    errors = []\n\n    for i, row in tqdm(df.iterrows(), total=len(df)):\n        if cfg.debug_on and i >= 1000:\n            break\n        \n        try:\n            samplename = row['filename'].split('/')[0]+'-'+row['filename'].split('/')[1].split('.')[0]\n            filepath = row['filename']\n            \n            mel_spec = process_audio_file(filepath, cfg)\n            \n            if mel_spec is not None:\n                all_bird_data[samplename] = mel_spec\n            \n        except Exception as e:\n            print(f\"Error processing {row.filepath}: {e}\")\n            errors.append((row.filepath, str(e)))\n\n    end_time = time.time()\n    print(f\"Processing completed in {end_time - start_time:.2f} seconds\")\n    print(f\"Successfully processed {len(all_bird_data)} files out of {len(df)}\")\n    print(f\"Failed to process {len(errors)} files\")\n\n    if save:\n        np.save(f'{os.path.join(cfg.output_dir,name)}.npy', all_bird_data)\n    \n    return all_bird_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.221010Z","iopub.execute_input":"2025-04-27T20:00:52.221230Z","iopub.status.idle":"2025-04-27T20:00:52.234988Z","shell.execute_reply.started":"2025-04-27T20:00:52.221211Z","shell.execute_reply":"2025-04-27T20:00:52.234118Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dataset\n# Sem nenhum data augmentation\n# Recebe train.csv e o dicionário dos spectrogramas e retorna o tensor dos spectrogramas e um tensor do one hot encoding das labels\n\nclass AudioDataset(Dataset):\n    def __init__(self, df, spectrograms = None, cfg = cfg):\n        self.df = df\n        self.cfg = cfg\n        self.spectrograms = spectrograms\n\n        self.species_ids = taxonomy['primary_label'].tolist()\n        self.num_classes = len(self.species_ids)\n        self.label_to_idx = {label: idx for idx, label in enumerate(self.species_ids)}\n        \n        if 'samplename' not in self.df.columns:\n            self.df['samplename'] = self.df.filename.map(lambda x: x.split('/')[0] + '-' + x.split('/')[-1].split('.')[0])\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        file = os.path.join(self.cfg.train_datadir, row['filename'])\n        label = row['primary_label']\n        samplename = row['samplename']\n\n        if self.spectrograms and samplename in self.spectrograms:\n            spec = self.spectrograms[samplename]\n            spec_tensor = torch.tensor(spec).float().unsqueeze(0)\n            label = self.encode_label(label)\n            return spec_tensor, label\n            \n        audio = process_audio_file(file, self.cfg)\n        spec = audio_to_melspec(audio, self.cfg)\n        spec_tensor = torch.tensor(spec).float().unsqueeze(0)\n        label = self.encode_label(label)\n        \n        return spec_tensor, label\n\n    # Retirada do Notebook do Kadircan İdrisoğlu\n    def encode_label(self, label):\n        \"\"\"Encode label to one-hot vector\"\"\"\n        target = np.zeros(self.num_classes)\n        if label in self.label_to_idx:\n            target[self.label_to_idx[label]] = 1.0\n        return target\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:19:51.930394Z","iopub.execute_input":"2025-04-27T20:19:51.930701Z","iopub.status.idle":"2025-04-27T20:19:51.938552Z","shell.execute_reply.started":"2025-04-27T20:19:51.930678Z","shell.execute_reply":"2025-04-27T20:19:51.937747Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CNN(nn.Module):\n    def __init__(self, cfg):\n        super().__init__()\n        self.cfg = cfg\n        self.num_classes = len(taxonomy) \n        # Backbone: Camadas iniciais de um modelo. Faz a extração das características\n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=cfg.is_pre_trained,                 # Se false, reseta os pesos pré treinados.\n            in_chans=cfg.input_channels,                   # Número de canais de entrada.\n            drop_rate=0.2,\n            drop_path_rate=0.2,\n        )\n\n        self.backbone = self.backbone.to(cfg.device)\n        \n        # Remove a camada final do backbone e passa a diante para conectar no meu nn.Linear\n        if 'efficientnet' in cfg.model_name:\n            backbone_out = self.backbone.classifier.in_features\n            self.backbone.classifier = nn.Identity()\n        elif 'resnet' in cfg.model_name:\n            backbone_out = self.backbone.fc.in_features\n            self.backbone.fc = nn.Identity()\n        else:\n            backbone_out = self.backbone.get_classifier().in_features\n            self.backbone.reset_classifier(0, '')\n\n        # Head: Parte da rede neural responsável por fazer a predição.\n        self.pooling = nn.AdaptiveAvgPool2d(1)         # Fixa o tamanho da feature map\n        self.feat_dim = backbone_out\n        self.classifier = nn.Linear(backbone_out, self.num_classes).to(cfg.device)    # Classificação final\n\n    def forward(self, x):\n        features = self.backbone(x)\n        if isinstance(features, dict):\n            features = features['features']\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size((0), -1))\n        logits = self.classifier(features)\n        return logits\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:00:52.260848Z","iopub.execute_input":"2025-04-27T20:00:52.261162Z","iopub.status.idle":"2025-04-27T20:00:52.274664Z","shell.execute_reply.started":"2025-04-27T20:00:52.261139Z","shell.execute_reply":"2025-04-27T20:00:52.274026Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Training loop\ndef training_loop(df, model, spectrograms, cfg):\n    model = model.to(cfg.device)\n    train_loss_values = []\n    valid_loss_values = []\n    \n    if cfg.criterion == 'BCEWithLogitsLoss':\n        criterion = nn.BCEWithLogitsLoss()\n        \n    if cfg.optimizer == 'AdamW':\n        optimizer = optim.Adam(\n                model.parameters(),\n                lr=cfg.lr,\n                weight_decay=cfg.weight_decay\n            )\n\n    labels = df['primary_label'].tolist()\n    \n    train_dataframe, val_dataframe = train_test_split(df, test_size=0.2, random_state=42, stratify=labels)\n\n    train_dataset = AudioDataset(train_dataframe, spectrograms = spectrograms, cfg=cfg)\n    val_dataset = AudioDataset(val_dataframe, spectrograms = spectrograms, cfg=cfg)\n    \n    train_loader = DataLoader(\n        train_dataset, \n        batch_size=cfg.batch_size, \n        shuffle=True, \n        pin_memory=True,\n        drop_last=True,\n    )\n    \n    valid_loader = DataLoader(\n        val_dataset, \n        batch_size=cfg.batch_size, \n        shuffle=False, \n        pin_memory=True,\n        drop_last=True,           #gera outputs e labels de tamanhos diferentes no último batch se drop_last = False\n    )\n\n    for epoch in tqdm(range(cfg.epochs)):   #tqdm serve para plotar uma barra de progessão para o treinamento\n        model.train()\n        running_loss = 0.0\n        all_train_outputs, all_train_labels, all_val_outputs, all_val_labels = [], [], [], []\n        \n        for inputs, labels in train_loader:\n            \n            inputs = inputs.to(cfg.device)\n            labels = labels.to(cfg.device)\n            \n            optimizer.zero_grad()\n            output = model(inputs)\n            loss = criterion(output, labels)\n            loss.backward()\n            optimizer.step()\n            \n            running_loss += loss.item()\n            all_train_labels.append(labels.detach().cpu().numpy())\n            all_train_outputs.append(output.detach().cpu().numpy())\n            \n        all_train_labels = np.concatenate(all_train_labels)\n        all_train_outputs = np.concatenate(all_train_outputs)\n        \n        avg_train_loss = running_loss / len(train_loader)\n        train_loss_values.append(avg_train_loss)\n        train_auc = calculate_auc(all_train_labels, all_train_outputs)\n                \n        model.eval()\n        running_loss = 0.0\n        with torch.no_grad():\n            for idx, (inputs, labels) in enumerate(valid_loader):\n                inputs = inputs.to(cfg.device)\n                labels = labels.to(cfg.device)\n                \n                outputs = model(inputs)\n                loss = criterion(outputs, labels)\n                \n                running_loss += loss.item()\n                all_val_labels.append(labels.detach().cpu().numpy())\n                all_val_outputs.append(output.detach().cpu().numpy())\n\n        \n        all_val_labels = np.concatenate(all_val_labels)\n        all_val_outputs = np.concatenate(all_val_outputs)\n        avg_valid_loss = running_loss / len(valid_loader)\n        valid_loss_values.append(avg_valid_loss)\n        val_auc = calculate_auc(all_val_labels, all_val_outputs)\n        \n        print(f\"Epoch {epoch+1}/{cfg.epochs} => Train Loss: {avg_train_loss:.4f}, Train AUC: {train_auc:.4f}, Validation Loss: {avg_valid_loss:.4f}, Validation AUC:{val_auc}\")\n\n    return train_loss_values, train_auc, valid_loss_values, val_auc\n\n\n# Retirada do Notebook do Kadircan İdrisoğlu\ndef calculate_auc(targets, outputs):\n    '''\n    Recebe dois np.2darray com todos os targets e outputs de cada batch de uma época\n    '''\n    num_classes = targets.shape[1]  # Nº de colunas do target\n    aucs = []\n    \n    probs = 1 / (1 + np.exp(-outputs)) # Função sigmoide\n    \n    for i in range(num_classes):\n        \n        if np.sum(targets[:, i]) > 0: \n            class_auc = roc_auc_score(targets[:, i], probs[:, i])\n            aucs.append(class_auc)\n    \n    return np.mean(aucs) if aucs else 0.0","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:10:44.066753Z","iopub.execute_input":"2025-04-27T20:10:44.067136Z","iopub.status.idle":"2025-04-27T20:10:44.078883Z","shell.execute_reply.started":"2025-04-27T20:10:44.067106Z","shell.execute_reply":"2025-04-27T20:10:44.078149Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loss, train_auc, val_loss, val_auc = training_loop(df, CNN(cfg), spectrograms = spectrograms, cfg = cfg)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:10:48.311322Z","iopub.execute_input":"2025-04-27T20:10:48.311635Z","iopub.status.idle":"2025-04-27T20:16:11.112823Z","shell.execute_reply.started":"2025-04-27T20:10:48.311610Z","shell.execute_reply":"2025-04-27T20:16:11.112063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.plot(train_loss, label='Train Loss')\nplt.plot(val_loss, label='Validation Loss')\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:16:26.857335Z","iopub.execute_input":"2025-04-27T20:16:26.857655Z","iopub.status.idle":"2025-04-27T20:16:27.018137Z","shell.execute_reply.started":"2025-04-27T20:16:26.857630Z","shell.execute_reply":"2025-04-27T20:16:27.017367Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#torch.save(CNN(cfg).state_dict(), os.path.join(cfg.output_dir, '1stModel.pth'))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-27T20:05:34.585065Z","iopub.status.idle":"2025-04-27T20:05:34.585341Z","shell.execute_reply":"2025-04-27T20:05:34.585224Z"}},"outputs":[],"execution_count":null}]}