{"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,"sourceType":"competition"},{"sourceId":11697088,"sourceType":"datasetVersion","datasetId":7341768}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport logging\nimport random\nimport gc\nimport time\nimport cv2\nimport math\nimport warnings\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score\nimport librosa \n\nimport torch\nimport torchvision\nimport torchvision.transforms as transforms \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\n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.auto import tqdm\n\nimport timm\n\nwarnings.filterwarnings(\"ignore\")\nlogging.basicConfig(level=logging.ERROR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:21.744253Z","iopub.execute_input":"2025-05-06T09:24:21.744848Z","iopub.status.idle":"2025-05-06T09:24:21.751947Z","shell.execute_reply.started":"2025-05-06T09:24:21.744818Z","shell.execute_reply":"2025-05-06T09:24:21.750613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CFG:\n    seed = 42\n    debug = False\n    apex = False\n    print_freq = 100\n    num_workers = 2 \n\n    OUTPUT_DIR = '/kaggle/working/'\n\n    # --- INPUT DATA ---\n    train_datadir = '/kaggle/input/birdclef-2025/train_audio' \n    train_csv = '/kaggle/input/birdclef-2025/train.csv'\n    taxonomy_csv = '/kaggle/input/birdclef-2025/taxonomy.csv'\n   \n    spectrogram_npy = '/kaggle/input/melspec-train-audio-update/birdclef25_melspec_5s_randcrop_32k_2048fft_512hop_128mel_rs256.npy'\n\n    # --- MODEL ---\n    model_name = 'efficientnet_b0'\n    pretrained = True\n    in_channels = 3 \n    dropout_prob = 0.3 \n\n    # --- DATA HANDLING ---\n    LOAD_DATA = True  \n    # --- Đồng bộ tham số Audio/Mel với transforming.py ---\n    FS = 32000\n    TARGET_DURATION = 5.0\n    N_FFT = 2048\n    HOP_LENGTH = 512\n    WIN_LENGTH = 2048 \n    N_MELS = 128\n    FMIN = 20\n    FMAX = 16000\n    TARGET_SHAPE = (256, 256) \n    # ------------------------------------------------------\n\n    # --- TRAINING ---\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    epochs = 10 \n    batch_size = 32\n    criterion = 'FocalLossBCE'\n    n_fold = 5\n    selected_folds = [0, 1, 2, 3, 4]\n    optimizer = 'AdamW'\n    lr = 3e-4\n    weight_decay = 1e-5\n    scheduler = 'CosineAnnealingLR'\n    min_lr = 1e-6\n    T_max = epochs\n\n    # --- AUGMENTATION ---\n    aug_prob = 0.5\n    mixup_alpha = 0.5\n\n    def update_debug_settings(self):\n        if self.debug:\n            self.epochs = 1\n            self.selected_folds = [0]\n            self.debug_limit = 1000 \n\ncfg = CFG()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:25.184323Z","iopub.execute_input":"2025-05-06T09:24:25.184842Z","iopub.status.idle":"2025-05-06T09:24:25.276070Z","shell.execute_reply.started":"2025-05-06T09:24:25.184818Z","shell.execute_reply":"2025-05-06T09:24:25.275298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"taxonomy_df_global = pd.read_csv(cfg.taxonomy_csv)\ncfg.num_classes = len(taxonomy_df_global)\ncfg.species_ids = taxonomy_df_global['primary_label'].tolist()\nprint(f\"Number of classes: {cfg.num_classes}\")\nprint(f\"Device: {cfg.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:28.566317Z","iopub.execute_input":"2025-05-06T09:24:28.567025Z","iopub.status.idle":"2025-05-06T09:24:28.590087Z","shell.execute_reply.started":"2025-05-06T09:24:28.566992Z","shell.execute_reply":"2025-05-06T09:24:28.589359Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(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.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-05-06T09:24:31.774695Z","iopub.execute_input":"2025-05-06T09:24:31.774981Z","iopub.status.idle":"2025-05-06T09:24:31.786179Z","shell.execute_reply.started":"2025-05-06T09:24:31.774959Z","shell.execute_reply":"2025-05-06T09:24:31.785410Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def audio2melspec(audio_data, cfg):\n    \"\"\"Convert audio data to mel spectrogram\"\"\"\n    if np.isnan(audio_data).any():\n        mean_signal = np.nanmean(audio_data)\n        audio_data = np.nan_to_num(audio_data, nan=mean_signal)\n\n    mel_spec = librosa.feature.melspectrogram(\n        y=audio_data,\n        sr=cfg.FS,\n        n_fft=cfg.N_FFT,\n        hop_length=cfg.HOP_LENGTH,\n        win_length=cfg.WIN_LENGTH,\n        n_mels=cfg.N_MELS,\n        fmin=cfg.FMIN,\n        fmax=cfg.FMAX,\n        power=2.0,\n        center=True,\n        pad_mode=\"reflect\",\n        norm='slaney',\n        htk=True,\n    )\n\n    mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)\n    mel_spec_norm = (mel_spec_db - mel_spec_db.min()) / (mel_spec_db.max() - mel_spec_db.min() + 1e-8)\n    \n    return mel_spec_norm\n\ndef process_audio_file(audio_path, cfg):\n    \"\"\"Process a single audio file to get the mel spectrogram\"\"\"\n    try:\n        audio_data, _ = librosa.load(audio_path, sr=cfg.FS)\n\n        target_samples = int(cfg.TARGET_DURATION * cfg.FS)\n\n        if len(audio_data) < target_samples:\n            n_copy = math.ceil(target_samples / len(audio_data))\n            if n_copy > 1:\n                audio_data = np.concatenate([audio_data] * n_copy)\n\n        # Extract center 5 seconds\n        start_idx = max(0, int(len(audio_data) / 2 - target_samples / 2))\n        end_idx = min(len(audio_data), start_idx + target_samples)\n        center_audio = audio_data[start_idx:end_idx]\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        mel_spec = audio2melspec(center_audio, cfg)\n        \n        if mel_spec.shape != cfg.TARGET_SHAPE:\n            mel_spec = cv2.resize(mel_spec, cfg.TARGET_SHAPE, interpolation=cv2.INTER_LINEAR)\n\n        return mel_spec.astype(np.float32)\n        \n    except Exception as e:\n        print(f\"Error processing {audio_path}: {e}\")\n        return None\n\ndef generate_spectrograms(df, cfg):\n    \"\"\"Generate spectrograms from audio files\"\"\"\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 and i >= 1000:\n            break\n        \n        try:\n            samplename = row['samplename']\n            filepath = row['filepath']\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    return all_bird_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:35.583708Z","iopub.execute_input":"2025-05-06T09:24:35.583981Z","iopub.status.idle":"2025-05-06T09:24:35.596403Z","shell.execute_reply.started":"2025-05-06T09:24:35.583955Z","shell.execute_reply":"2025-05-06T09:24:35.595661Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFDatasetFromNPY(Dataset):\n    def __init__(self, df, cfg, spectrograms=None, mode=\"train\"):\n        self.df = df\n        self.cfg = cfg\n        self.mode = mode\n        self.spectrograms = spectrograms\n\n        self.species_ids = cfg.species_ids\n        self.num_classes = cfg.num_classes\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.replace('.ogg',''))\n\n        # Định nghĩa transform cho normalization (áp dụng cho cả train/valid)\n        self.normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n\n        # Kiểm tra số lượng spectrograms khớp\n        if self.spectrograms:\n            sample_names_df = set(self.df['samplename'])\n            found_samples = sum(1 for name in sample_names_df if name in self.spectrograms)\n            missing_samples = len(sample_names_df) - found_samples\n            print(f\"Dataset '{mode}': Found {found_samples} matching spectrograms out of {len(sample_names_df)} unique samples.\")\n            if missing_samples > 0:\n                print(f\"Warning: {missing_samples} samples in the dataframe partition do not have matching spectrograms.\")\n        else:\n             print(f\"Warning: No spectrogram dictionary provided for dataset '{mode}'.\")\n\n\n        if cfg.debug and hasattr(cfg, 'debug_limit'):\n            self.df = self.df.sample(min(cfg.debug_limit, len(self.df)), random_state=cfg.seed).reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        samplename = row['samplename']\n        spec = None\n\n        if self.spectrograms and samplename in self.spectrograms:\n            spec = self.spectrograms[samplename]\n        else:\n            # Xử lý trường hợp thiếu spec (ví dụ: trả về spec toàn 0)\n            # print(f\"Warning: Spectrogram for {samplename} not found. Returning zeros.\")\n            # Cần shape gốc trước khi repeat kênh\n            spec = np.zeros(self.cfg.TARGET_SHAPE, dtype=np.float32)\n\n        # Đảm bảo spec có đúng shape trước khi repeat\n        if spec.shape != self.cfg.TARGET_SHAPE:\n             # print(f\"Warning: Spectrogram for {samplename} has shape {spec.shape}, expected {self.cfg.TARGET_SHAPE}. Resizing/Padding might be needed or NPY mismatch.\")\n             if spec.shape[1] > 0 and spec.shape[0] > 0:\n                 spec = cv2.resize(spec, (self.cfg.TARGET_SHAPE[1], self.cfg.TARGET_SHAPE[0]), interpolation=cv2.INTER_LINEAR)\n             else: # Nếu shape không hợp lệ, trả về zero\n                 spec = np.zeros(self.cfg.TARGET_SHAPE, dtype=np.float32)\n\n\n        # *** Chuyển sang Tensor 3 kênh ***\n        if len(spec.shape) == 3: \n             spec = spec.squeeze()\n        if len(spec.shape) != 2:\n             print(f\"Error: Unexpected shape for spec {samplename}: {spec.shape}. Returning None.\")\n             return None \n\n        spec_tensor = torch.tensor(spec, dtype=torch.float32).unsqueeze(0).repeat(3, 1, 1) # Tạo (3, H, W)\n\n        # *** Áp dụng ImageNet Normalization ***\n        spec_tensor = self.normalize(spec_tensor)\n\n        # Áp dụng augmentations trên spectrogram (sau normalization)\n        if self.mode == \"train\" and random.random() < self.cfg.aug_prob:\n            spec_tensor = self.apply_spec_augmentations(spec_tensor)\n\n        target = self.encode_label(row['primary_label'])\n\n        # Xử lý secondary_labels\n        if 'secondary_labels' in row and isinstance(row['secondary_labels'], str) and row['secondary_labels'] != '[]':\n             try:\n                 secondary_labels = eval(row['secondary_labels'])\n                 for label in secondary_labels:\n                     if label in self.label_to_idx:\n                         target[self.label_to_idx[label]] = 1.0\n             except Exception:\n                 pass \n        elif 'secondary_labels' in row and isinstance(row['secondary_labels'], list) and row['secondary_labels']:\n             for label in row['secondary_labels']:\n                 if label in self.label_to_idx:\n                     target[self.label_to_idx[label]] = 1.0\n\n        return {\n            'melspec': spec_tensor,\n            'target': torch.tensor(target, dtype=torch.float32),\n            'filename': row['filename']\n        }\n\n    def apply_spec_augmentations(self, spec):\n        # Time masking\n        if random.random() < 0.5:\n            num_masks = random.randint(1, 3)\n            for _ in range(num_masks):\n                width = random.randint(5, 30) # Tăng nhẹ max width\n                start = random.randint(0, max(0, spec.shape[2] - width)) \n                if width > 0 : spec[:, :, start:start+width] = 0\n\n        # Frequency masking\n        if random.random() < 0.5:\n            num_masks = random.randint(1, 3)\n            for _ in range(num_masks):\n                height = random.randint(5, 30) # Tăng nhẹ max height\n                start = random.randint(0, max(0, spec.shape[1] - height)) \n                if height > 0: spec[:, start:start+height, :] = 0\n\n        # Thêm RandomErasing\n        # spec = transforms.RandomErasing(p=0.3, scale=(0.02, 0.1))(spec)\n\n        return spec\n\n    def encode_label(self, label):\n        target = np.zeros(self.num_classes, dtype=np.float32)\n        if label in self.label_to_idx:\n            target[self.label_to_idx[label]] = 1.0\n        return target\n\n\ndef collate_fn(batch):\n    # Lọc ra các item không hợp lệ (trả về None từ __getitem__)\n    batch = [item for item in batch if item is not None]\n    if not batch:\n        return None \n\n    elem = batch[0]\n    melspecs = torch.stack([item['melspec'] for item in batch])\n    targets = torch.stack([item['target'] for item in batch])\n    filenames = [item['filename'] for item in batch]\n\n    return {'melspec': melspecs, 'target': targets, 'filename': filenames}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:38.634779Z","iopub.execute_input":"2025-05-06T09:24:38.635469Z","iopub.status.idle":"2025-05-06T09:24:38.664291Z","shell.execute_reply.started":"2025-05-06T09:24:38.635430Z","shell.execute_reply":"2025-05-06T09:24:38.663360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdCLEFModel(nn.Module):\n    def __init__(self, cfg, num_classes):\n        super().__init__()\n        self.cfg = cfg\n\n        self.backbone = timm.create_model(\n            cfg.model_name,\n            pretrained=cfg.pretrained,\n            in_chans=cfg.in_channels, # *** Sử dụng 3 kênh ***\n            drop_rate=0.2,\n            drop_path_rate=0.2\n        )\n\n        # Lấy số features output từ backbone\n        if hasattr(self.backbone, 'get_classifier'):\n             backbone_out = self.backbone.get_classifier().in_features\n        elif hasattr(self.backbone, 'head') and hasattr(self.backbone.head, 'in_features'): # Xử lý các kiến trúc khác như Swin\n             backbone_out = self.backbone.head.in_features\n        elif hasattr(self.backbone, 'fc') and hasattr(self.backbone.fc, 'in_features'):\n            backbone_out = self.backbone.fc.in_features\n        elif hasattr(self.backbone, 'classifier') and hasattr(self.backbone.classifier, 'in_features'):\n            backbone_out = self.backbone.classifier.in_features\n        else: \n            try:\n                backbone_out = self.backbone.num_features\n            except AttributeError:\n                raise ValueError(f\"Không thể tự động xác định output features cho model {cfg.model_name}\")\n\n        # Reset classifier gốc của timm model\n        if hasattr(self.backbone, 'reset_classifier'):\n            self.backbone.reset_classifier(0, '')\n        elif hasattr(self.backbone, 'head'):\n            self.backbone.head = nn.Identity()\n        elif hasattr(self.backbone, 'fc'):\n            self.backbone.fc = nn.Identity()\n        elif hasattr(self.backbone, 'classifier'):\n             self.backbone.classifier = nn.Identity()\n\n        self.pooling = nn.AdaptiveAvgPool2d(1)\n        # *** Thêm lớp Dropout ***\n        self.dropout = nn.Dropout(p=cfg.dropout_prob)\n        self.classifier = nn.Linear(backbone_out, num_classes)\n\n        # Mixup config\n        self.mixup_enabled = hasattr(cfg, 'mixup_alpha') and cfg.mixup_alpha > 0\n        if self.mixup_enabled:\n            self.mixup_alpha = cfg.mixup_alpha\n\n    def forward(self, x, targets=None):\n        targets_a, targets_b, lam = None, None, None \n\n        if self.training and self.mixup_enabled and targets is not None:\n            mixed_x, targets_a, targets_b, lam = self.mixup_data(x, targets)\n            x = mixed_x\n\n        features = self.backbone(x)\n\n        if isinstance(features, dict):\n             features = features.get('features', features.get('head_output', next(iter(features.values()))))\n\n        if len(features.shape) == 4:\n            features = self.pooling(features)\n            features = features.view(features.size(0), -1) # Flatten\n\n        # *** Áp dụng Dropout ***\n        features = self.dropout(features)\n\n        logits = self.classifier(features)\n\n        if self.training:\n            if self.mixup_enabled and targets is not None:\n                 return logits, targets_a, targets_b, lam\n            else:\n                 return logits, targets, None, None \n        else: # Khi eval\n             return logits\n\n    def mixup_data(self, x, targets):\n        batch_size = x.size(0)\n        alpha = self.mixup_alpha if self.mixup_alpha > 0 else 1e-6\n        lam = np.random.beta(alpha, alpha)\n        indices = torch.randperm(batch_size, device=x.device)\n        mixed_x = lam * x + (1 - lam) * x[indices]\n        return mixed_x, targets, targets[indices], lam","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:24:44.280892Z","iopub.execute_input":"2025-05-06T09:24:44.281425Z","iopub.status.idle":"2025-05-06T09:24:44.292529Z","shell.execute_reply.started":"2025-05-06T09:24:44.281401Z","shell.execute_reply":"2025-05-06T09:24:44.291755Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_optimizer(model, cfg):\n    if cfg.optimizer == 'Adam':\n        optimizer = optim.Adam(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\n    elif cfg.optimizer == 'AdamW':\n        optimizer = optim.AdamW(model.parameters(), lr=cfg.lr, weight_decay=cfg.weight_decay)\n    elif cfg.optimizer == 'SGD':\n        optimizer = optim.SGD(model.parameters(), lr=cfg.lr, momentum=0.9, weight_decay=cfg.weight_decay)\n    else:\n        raise NotImplementedError(f\"Optimizer {cfg.optimizer} not implemented\")\n    return optimizer\n\n# --- Scheduler ---\ndef get_scheduler(optimizer, cfg):\n    if cfg.scheduler == 'CosineAnnealingLR':\n        scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=cfg.T_max, eta_min=cfg.min_lr)\n    elif cfg.scheduler == 'ReduceLROnPlateau':\n        # Chú ý mode='max' vì chúng ta theo dõi AUC\n        scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2, min_lr=cfg.min_lr, verbose=True)\n    elif cfg.scheduler == 'StepLR':\n        scheduler = lr_scheduler.StepLR(optimizer, step_size=cfg.epochs // 3, gamma=0.5)\n    elif cfg.scheduler == 'OneCycleLR':\n        scheduler = None \n    else:\n        scheduler = None\n    return scheduler\n\n# --- Loss Function ---\nclass FocalLossBCE(torch.nn.Module):\n    def __init__(self, alpha: float = 0.25, gamma: float = 2, reduction: str = \"mean\", bce_weight: float = 0.5, focal_weight: float = 0.5): # Điều chỉnh weight nếu muốn\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.reduction = reduction\n        self.bce = torch.nn.BCEWithLogitsLoss(reduction='none') # Tính loss cho từng sample/class\n        self.focal_weight = focal_weight\n        self.bce_weight = bce_weight\n\n    def forward(self, logits, targets):\n        bce_loss = self.bce(logits, targets)\n\n        # Sigmoid focal loss tính toán nội bộ sigmoid\n        focal_loss = torchvision.ops.sigmoid_focal_loss(\n            inputs=logits,\n            targets=targets,\n            alpha=self.alpha,\n            gamma=self.gamma,\n            reduction='none', # Tính loss cho từng sample/class\n        )\n\n        # Kết hợp loss\n        combined_loss = self.bce_weight * bce_loss + self.focal_weight * focal_loss\n\n        # Áp dụng reduction cuối cùng\n        if self.reduction == \"mean\":\n            return combined_loss.mean()\n        elif self.reduction == \"sum\":\n            return combined_loss.sum()\n        else: # 'none'\n            return combined_loss\n\ndef get_criterion(cfg):\n    if cfg.criterion == 'BCEWithLogitsLoss':\n        criterion = nn.BCEWithLogitsLoss()\n    elif cfg.criterion == 'FocalLossBCE':\n        criterion = FocalLossBCE(bce_weight=0.5, focal_weight=0.5)\n    else:\n        raise NotImplementedError(f\"Criterion {cfg.criterion} not implemented\")\n    return criterion","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:34:51.985193Z","iopub.execute_input":"2025-05-06T09:34:51.985901Z","iopub.status.idle":"2025-05-06T09:34:51.995711Z","shell.execute_reply.started":"2025-05-06T09:34:51.985878Z","shell.execute_reply":"2025-05-06T09:34:51.995023Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, optimizer, criterion, device, scheduler=None):\n    model.train()\n    losses = []\n    all_targets_np = []\n    all_outputs_np = []\n\n    pbar = tqdm(enumerate(loader), total=len(loader), desc=\"Training\", leave=False)\n\n    for step, batch in pbar:\n        if batch is None: continue\n\n        inputs = batch['melspec'].to(device)\n        targets_orig = batch['target'].to(device)\n\n        optimizer.zero_grad()\n        outputs, targets_a, targets_b, lam = model(inputs, targets_orig)\n\n        if targets_a is not None: # Mixup\n            loss = lam * criterion(outputs, targets_a) + (1 - lam) * criterion(outputs, targets_b)\n            targets_for_auc = targets_orig \n        else: # No mixup\n            loss = criterion(outputs, targets_orig)\n            targets_for_auc = targets_orig\n\n        if not torch.isnan(loss) and not torch.isinf(loss):\n            loss.backward()\n            # Optional: Gradient clipping\n            # torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            optimizer.step()\n            losses.append(loss.item())\n        else:\n            print(f\"Warning: NaN/Inf loss detected at step {step}. Skipping step.\")\n            losses.append(np.nan) # Ghi nhận NaN\n\n        if scheduler is not None and isinstance(scheduler, lr_scheduler.OneCycleLR):\n            scheduler.step()\n\n        all_outputs_np.append(outputs.detach().cpu().numpy())\n        all_targets_np.append(targets_for_auc.detach().cpu().numpy())\n\n        pbar.set_postfix({\n            'train_loss': f\"{np.nanmean(losses[-50:]):.4f}\" if losses else 'N/A',\n            'lr': f\"{optimizer.param_groups[0]['lr']:.2e}\"\n        })\n\n    if not all_outputs_np:\n        print(\"Warning: No valid batches processed in this epoch.\")\n        return 0.0, 0.0\n\n    all_outputs_np = np.concatenate(all_outputs_np)\n    all_targets_np = np.concatenate(all_targets_np)\n\n    # Lọc NaN trước khi tính AUC\n    valid_idx = ~np.isnan(all_outputs_np).any(axis=1)\n    if not np.any(valid_idx):\n         print(\"Warning: All outputs are NaN. Cannot calculate AUC.\")\n         return np.nanmean(losses), 0.0\n\n    auc = calculate_auc(all_targets_np[valid_idx], all_outputs_np[valid_idx])\n    avg_loss = np.nanmean(losses) # Tính trung bình bỏ qua NaN\n\n    return avg_loss, auc\n\n\ndef validate(model, loader, criterion, device):\n    model.eval()\n    losses = []\n    all_targets_np = []\n    all_outputs_np = []\n\n    with torch.no_grad():\n        for batch in tqdm(loader, desc=\"Validation\", leave=False):\n            if batch is None: continue\n\n            inputs = batch['melspec'].to(device)\n            targets = batch['target'].to(device)\n\n            outputs = model(inputs) # model.eval() chỉ trả về logits\n            loss = criterion(outputs, targets)\n\n            if not torch.isnan(loss) and not torch.isinf(loss):\n                 losses.append(loss.item())\n            else:\n                 losses.append(np.nan)\n\n            all_outputs_np.append(outputs.cpu().numpy())\n            all_targets_np.append(targets.cpu().numpy())\n\n    if not all_outputs_np:\n        print(\"Warning: No valid batches processed in validation.\")\n        return 0.0, 0.0\n\n    all_outputs_np = np.concatenate(all_outputs_np)\n    all_targets_np = np.concatenate(all_targets_np)\n\n    valid_idx = ~np.isnan(all_outputs_np).any(axis=1)\n    if not np.any(valid_idx):\n         print(\"Warning: All validation outputs are NaN. Cannot calculate AUC.\")\n         return np.nanmean(losses), 0.0\n\n    auc = calculate_auc(all_targets_np[valid_idx], all_outputs_np[valid_idx])\n    avg_loss = np.nanmean(losses)\n\n    return avg_loss, auc\n\ndef calculate_auc(targets, outputs):\n    num_classes = targets.shape[1]\n    aucs = []\n    # Áp dụng sigmoid cho logits để có xác suất\n    probs = 1 / (1 + np.exp(-outputs))\n\n    for i in range(num_classes):\n        target_class = targets[:, i]\n        if np.sum(target_class) > 0 and np.sum(target_class) < len(target_class):\n            try:\n                class_auc = roc_auc_score(target_class, probs[:, i])\n                aucs.append(class_auc)\n            except ValueError as e:\n                # print(f\"Skipping AUC for class {i} due to error: {e}\")\n                aucs.append(0.5) # Hoặc np.nan\n        # elif np.sum(target_class) == 0: # Nếu lớp không có trong validation fold\n        #      pass # \n        # else: # Nếu lớp chỉ có toàn 1 \n        #      aucs.append(0.5) # \n\n    return np.mean(aucs) if aucs else 0.0 # Trả về 0 nếu không có lớp nào hợp lệ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:34:56.389172Z","iopub.execute_input":"2025-05-06T09:34:56.389486Z","iopub.status.idle":"2025-05-06T09:34:56.403181Z","shell.execute_reply.started":"2025-05-06T09:34:56.389456Z","shell.execute_reply":"2025-05-06T09:34:56.402641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_training(df, cfg):\n    if cfg.debug:\n        cfg.update_debug_settings()\n\n    spectrograms = None\n    if cfg.LOAD_DATA:\n        print(f\"Loading pre-computed mel spectrograms from: {cfg.spectrogram_npy}\")\n        npy_path = Path(cfg.spectrogram_npy)\n        if not npy_path.is_file():\n             print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n             print(f\"ERROR: Spectrogram file NOT FOUND at {cfg.spectrogram_npy}\")\n             print(f\"Please verify the path and ensure the transforming script ran successfully.\")\n             print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n             return # Thoát nếu không tìm thấy file\n        try:\n            # Ghi nhớ thời gian load\n            load_start = time.time()\n            spectrograms = np.load(cfg.spectrogram_npy, allow_pickle=True).item()\n            load_end = time.time()\n            print(f\"Loaded {len(spectrograms)} pre-computed mel spectrograms in {load_end - load_start:.2f} seconds.\")\n            if not spectrograms:\n                 print(\"ERROR: Loaded spectrogram dictionary is empty!\")\n                 return\n\n            # Kiểm tra shape của một sample\n            first_key = next(iter(spectrograms))\n            first_spec_shape = spectrograms[first_key].shape\n            print(f\"Shape of first spectrogram ('{first_key}'): {first_spec_shape}\")\n            # *** Kiểm tra shape có khớp với TARGET_SHAPE không ***\n            if cfg.TARGET_SHAPE != first_spec_shape:\n                 print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n                 print(f\"WARNING: Spectrogram shape in NPY {first_spec_shape} does NOT match CFG.TARGET_SHAPE {cfg.TARGET_SHAPE}!\")\n                 print(f\"Ensure TARGET_SHAPE in this notebook matches the output shape of transforming.py.\")\n                 print(f\"         (Remember that resize happens in transforming.py if DO_RESIZE=True)\")\n                 print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n                 # return\n\n        except Exception as e:\n            print(f\"Error loading pre-computed spectrograms: {e}\")\n            return \n    else:\n        print(\"LOAD_DATA is False. Spectrograms will be generated on-the-fly.\")\n\n    skf = StratifiedKFold(n_splits=cfg.n_fold, shuffle=True, random_state=cfg.seed)\n    oof_scores = [] \n\n    # --- Vòng lặp qua các Fold ---\n    for fold, (train_idx, val_idx) in enumerate(skf.split(df, df['primary_label'])):\n        if fold not in cfg.selected_folds:\n            continue\n\n        print(f'\\n{\"=\"*30} Fold {fold} {\"=\"*30}')\n        train_df_fold = df.iloc[train_idx].reset_index(drop=True)\n        val_df_fold = df.iloc[val_idx].reset_index(drop=True)\n\n        print(f'Training set samples: {len(train_df_fold)}')\n        print(f'Validation set samples: {len(val_df_fold)}')\n\n        # Tạo Datasets và DataLoaders cho fold hiện tại\n        train_dataset = BirdCLEFDatasetFromNPY(train_df_fold, cfg, spectrograms=spectrograms, mode='train')\n        val_dataset = BirdCLEFDatasetFromNPY(val_df_fold, cfg, spectrograms=spectrograms, mode='valid')\n\n        train_loader = DataLoader(train_dataset, batch_size=cfg.batch_size, shuffle=True,\n                                num_workers=cfg.num_workers, pin_memory=True, collate_fn=collate_fn, drop_last=True)\n        val_loader = DataLoader(val_dataset, batch_size=cfg.batch_size * 2, shuffle=False,\n                              num_workers=cfg.num_workers, pin_memory=True, collate_fn=collate_fn)\n\n        # Khởi tạo model, optimizer, criterion, scheduler cho fold\n        # *** Truyền cfg.num_classes vào model ***\n        model = BirdCLEFModel(cfg, cfg.num_classes).to(cfg.device)\n        optimizer = get_optimizer(model, cfg)\n        criterion = get_criterion(cfg)\n\n        if cfg.scheduler == 'OneCycleLR':\n            steps_per_epoch = len(train_loader)\n            if steps_per_epoch == 0:\n                 print(\"Warning: train_loader is empty. Cannot initialize OneCycleLR.\")\n                 scheduler = None\n            else:\n                 scheduler = lr_scheduler.OneCycleLR(optimizer, max_lr=cfg.lr, steps_per_epoch=steps_per_epoch, epochs=cfg.epochs, pct_start=0.1)\n        else:\n            scheduler = get_scheduler(optimizer, cfg)\n\n        best_fold_auc = 0\n        best_epoch = 0\n        fold_start_time = time.time()\n\n        # --- Vòng lặp qua các Epoch ---\n        for epoch in range(cfg.epochs):\n            epoch_start_time = time.time()\n            print(f\"\\nEpoch {epoch+1}/{cfg.epochs}\")\n\n            train_loss, train_auc = train_one_epoch(model, train_loader, optimizer, criterion, cfg.device, scheduler if isinstance(scheduler, lr_scheduler.OneCycleLR) else None)\n            val_loss, val_auc = validate(model, val_loader, criterion, cfg.device)\n\n            epoch_end_time = time.time()\n            epoch_duration = epoch_end_time - epoch_start_time\n\n            if scheduler is not None and not isinstance(scheduler, lr_scheduler.OneCycleLR):\n                if isinstance(scheduler, lr_scheduler.ReduceLROnPlateau):\n                    scheduler.step(val_auc) # Step dựa trên validation AUC\n                else:\n                    scheduler.step()\n\n            print(f\"Epoch {epoch+1} Summary:\")\n            print(f\"  Time: {epoch_duration:.2f}s\")\n            print(f\"  Train Loss: {train_loss:.4f}, Train AUC: {train_auc:.4f}\")\n            print(f\"  Val Loss  : {val_loss:.4f}, Val AUC  : {val_auc:.4f}\")\n\n            # Lưu model tốt nhất của fold này\n            if val_auc > best_fold_auc:\n                best_fold_auc = val_auc\n                best_epoch = epoch + 1\n                print(f\"  >>> New best AUC for Fold {fold}: {best_fold_auc:.4f} at epoch {best_epoch} <<<\")\n\n                checkpoint_payload = {\n                    'model_state_dict': model.state_dict(),\n                    'optimizer_state_dict': optimizer.state_dict(),\n                    'epoch': epoch,\n                    'val_auc': val_auc,\n                    'train_auc': train_auc,\n                    'cfg': {\n                        'model_name': cfg.model_name,\n                        'in_channels': cfg.in_channels,\n                        'num_classes': cfg.num_classes,\n                        'TARGET_SHAPE': cfg.TARGET_SHAPE,\n                        'N_FFT': cfg.N_FFT,\n                        'HOP_LENGTH': cfg.HOP_LENGTH,\n                        'N_MELS': cfg.N_MELS,\n                    }\n                }\n                if scheduler:\n                     checkpoint_payload['scheduler_state_dict'] = scheduler.state_dict()\n\n                torch.save(checkpoint_payload, f\"{cfg.OUTPUT_DIR}model_fold{fold}_best.pth\")\n                print(f\"  Saved best model checkpoint for Fold {fold}\")\n\n        fold_end_time = time.time()\n        print(f\"\\nFinished Fold {fold} in {(fold_end_time - fold_start_time)/60:.2f} minutes. Best Val AUC: {best_fold_auc:.4f} at epoch {best_epoch}\")\n        oof_scores.append(best_fold_auc)\n\n        del model, optimizer, scheduler, train_loader, val_loader, train_dataset, val_dataset\n        if cfg.device == 'cuda':\n             torch.cuda.empty_cache()\n        gc.collect()\n\n    # --- Conclusion CV ---\n    print(\"\\n\" + \"=\"*60)\n    print(\"Cross-Validation Results:\")\n    for i, fold_idx in enumerate(cfg.selected_folds):\n        if i < len(oof_scores):\n             print(f\"Fold {fold_idx}: {oof_scores[i]:.4f}\")\n        else:\n             print(f\"Fold {fold_idx}: Not run or score not available\")\n    if oof_scores:\n        print(f\"Mean OOF AUC: {np.mean(oof_scores):.4f}\")\n    print(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:35:01.480238Z","iopub.execute_input":"2025-05-06T09:35:01.480510Z","iopub.status.idle":"2025-05-06T09:35:01.496433Z","shell.execute_reply.started":"2025-05-06T09:35:01.480492Z","shell.execute_reply":"2025-05-06T09:35:01.495604Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    print(\"\\nLoading training data metadata...\")\n    train_df_main = pd.read_csv(cfg.train_csv)\n\n    if cfg.LOAD_DATA and not Path(cfg.spectrogram_npy).is_file():\n         print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n         print(f\"ERROR: Spectrogram file NOT FOUND at {cfg.spectrogram_npy}\")\n         print(f\"Please ensure the path is correct and the file exists before running training.\")\n         print(f\"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\")\n    else:\n         print(\"\\nStarting training...\")\n         run_training(train_df_main, cfg)\n         print(\"\\nTraining complete!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-06T09:35:05.801365Z","iopub.execute_input":"2025-05-06T09:35:05.801855Z","execution_failed":"2025-05-06T15:03:33.108Z"}},"outputs":[],"execution_count":null}]}