{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":91844,"databundleVersionId":11361821,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport random\nimport gc\nimport glob\n\nimport numpy as np\nimport pandas as pd\nimport librosa\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nimport torch\nimport torch.nn as nn\nimport torchvision\nimport torchaudio\nimport timm\n\nfrom torch.utils.data import DataLoader, Dataset\nfrom torch.optim import AdamW\nfrom torch.cuda.amp import GradScaler, autocast\n\nfrom sklearn.metrics import (\n    precision_recall_fscore_support,\n    accuracy_score,\n    roc_auc_score,\n    roc_curve\n)\nfrom sklearn.model_selection import StratifiedKFold\n\nfrom tqdm import tqdm\nfrom warnings import filterwarnings\nfilterwarnings(\"ignore\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.551323Z","iopub.execute_input":"2025-05-19T12:39:14.551859Z","iopub.status.idle":"2025-05-19T12:39:14.557051Z","shell.execute_reply.started":"2025-05-19T12:39:14.551812Z","shell.execute_reply":"2025-05-19T12:39:14.556403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Config:\n    train_dir       = \"/kaggle/input/birdclef-2025/train_audio\"\n    train_csv       = \"/kaggle/input/birdclef-2025/train.csv\"\n    \n    sr              = 32_000\n    n_fft           = 1024\n    hop_length      = 500\n    n_mels          = 128\n    fmin            = 50\n    fmax            = 16_000\n    power           = 2\n    \n    seed            = 42\n    num_classes     = 206\n    batch_size      = 64\n    num_workers     = 2\n    epochs          = 20\n    lr_max          = 1e-4\n    weight_decay    = 1e-6\n    use_amp         = True\n    mixup_alpha     = 0.5\n    \n    # threshold for binarizing sigmoid outputs\n    pred_thresh     = 0.5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.558154Z","iopub.execute_input":"2025-05-19T12:39:14.558404Z","iopub.status.idle":"2025-05-19T12:39:14.576760Z","shell.execute_reply.started":"2025-05-19T12:39:14.558388Z","shell.execute_reply":"2025-05-19T12:39:14.576257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(s=Config.seed):\n    random.seed(s)\n    np.random.seed(s)\n    torch.manual_seed(s)\n    torch.cuda.manual_seed_all(s)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\nset_seed()\n\n# load metadata\ndf = pd.read_csv(Config.train_csv)\ndf['filename'] = df['filename'].apply(lambda x: os.path.join(Config.train_dir, x))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.577379Z","iopub.execute_input":"2025-05-19T12:39:14.577556Z","iopub.status.idle":"2025-05-19T12:39:14.723065Z","shell.execute_reply.started":"2025-05-19T12:39:14.577538Z","shell.execute_reply":"2025-05-19T12:39:14.722532Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BirdClefDataset(Dataset):\n    def __init__(self, df, mode='train'):\n        self.df   = df.reset_index(drop=True)\n        self.mode = mode\n\n    def __len__(self):\n        return len(self.df)\n\n    def _load_audio(self, path):\n        y, _ = librosa.load(path, sr=Config.sr)\n        # pad / trim to exactly 10 seconds\n        target_len = 10 * Config.sr\n        if len(y) < target_len:\n            y = np.tile(y, int(np.ceil(target_len / len(y))))\n        y = y[:target_len]\n        return y\n\n    def _to_melspec(self, y):\n        S = librosa.feature.melspectrogram(\n            y=y, sr=Config.sr,\n            n_fft=Config.n_fft,\n            hop_length=Config.hop_length,\n            n_mels=Config.n_mels,\n            fmin=Config.fmin,\n            fmax=Config.fmax,\n            power=Config.power\n        )\n        S = librosa.power_to_db(S, ref=np.max)\n        # normalize to [0,1]\n        S = (S - S.min()) / (S.max() - S.min() + 1e-6)\n        # to 3-channel by duplicating\n        img = np.stack([S, S, S], axis=0)\n        return img.astype(np.float32)\n\n    def __getitem__(self, idx):\n        row = self.df.loc[idx]\n        y   = self._load_audio(row.filename)\n        x   = self._to_melspec(y)\n        if self.mode == 'train':\n            # single-label primary_label\n            label = row.primary_label\n            # map label to int 0..205\n            target = label_mapper[label]\n            return x, target\n        else:\n            return x\n\n\n# build label mapper\nlabels = sorted(df.primary_label.unique())\nlabel_mapper = {lab:i for i,lab in enumerate(labels)}\nrev_mapper   = {i:lab for lab,i in label_mapper.items()}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.723669Z","iopub.execute_input":"2025-05-19T12:39:14.723875Z","iopub.status.idle":"2025-05-19T12:39:14.734415Z","shell.execute_reply.started":"2025-05-19T12:39:14.723854Z","shell.execute_reply":"2025-05-19T12:39:14.733868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GeM(nn.Module):\n    def __init__(self, p=3.0, eps=1e-6):\n        super().__init__()\n        self.p   = nn.Parameter(torch.ones(1)*p)\n        self.eps = eps\n    def forward(self, x):\n        return torch.nn.functional.adaptive_avg_pool2d(x.clamp(min=self.eps).pow(self.p),\n                                                     (1,1)).pow(1./self.p).view(x.size(0), -1)\n\n\nclass BirdCLEFNet(nn.Module):\n    def __init__(self, backbone='tf_efficientnet_b0', pretrained=False):\n        super().__init__()\n        # extract channels from intermediate layers 3 and 4\n        self.feat_extractor = timm.create_model(\n            backbone, pretrained=pretrained,\n            features_only=True, out_indices=(3,4),\n            in_chans=3\n        )\n        chans = self.feat_extractor.feature_info.channels()  # e.g. [80, 320]\n        self.gpools = nn.ModuleList([GeM() for _ in chans])\n        self.bn     = nn.BatchNorm1d(sum(chans))\n        self.fc     = nn.Linear(sum(chans), Config.num_classes)\n\n    def forward(self, x):\n        feats = self.feat_extractor(x)              # list of 2 feature maps\n        pooled = [g(f) for g,f in zip(self.gpools, feats)]\n        h = torch.cat(pooled, dim=1)\n        return self.fc(self.bn(h))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.735791Z","iopub.execute_input":"2025-05-19T12:39:14.736077Z","iopub.status.idle":"2025-05-19T12:39:14.757361Z","shell.execute_reply.started":"2025-05-19T12:39:14.736057Z","shell.execute_reply":"2025-05-19T12:39:14.756770Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class FocalBCE(nn.Module):\n    def __init__(self, alpha=0.25, gamma=2.0, reduction='mean'):\n        super().__init__()\n        self.alpha = alpha\n        self.gamma = gamma\n        self.bce   = nn.BCEWithLogitsLoss(reduction=reduction)\n\n    def forward(self, logits, targets):\n        bce = self.bce(logits, targets)\n        # focal component\n        prob = torch.sigmoid(logits)\n        p_t  = prob*targets + (1-prob)*(1-targets)\n        focal = ((1-p_t)**self.gamma * \n                 (-self.alpha*targets*torch.log(prob+1e-6) \n                  - (1-self.alpha)*(1-targets)*torch.log(1-prob+1e-6))).mean()\n        return bce + focal\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.757998Z","iopub.execute_input":"2025-05-19T12:39:14.758200Z","iopub.status.idle":"2025-05-19T12:39:14.774687Z","shell.execute_reply.started":"2025-05-19T12:39:14.758186Z","shell.execute_reply":"2025-05-19T12:39:14.774180Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_metrics(y_true, y_prob, thresh=0.5):\n    y_pred = (y_prob >= thresh).astype(int)\n    \n    p_m, r_m, f_m, _ = precision_recall_fscore_support(\n        y_true, y_pred, average='macro', zero_division=0)\n    p_w, r_w, f_w, _ = precision_recall_fscore_support(\n        y_true, y_pred, average='weighted', zero_division=0)\n    \n    # subset accuracy\n    acc = accuracy_score(y_true, y_pred)\n    \n    n_samples = y_true.shape[0]\n    valid = np.where((y_true.sum(axis=0) > 0) & (y_true.sum(axis=0) < n_samples))[0]\n    if len(valid) > 0:\n        roc = roc_auc_score(y_true[:, valid], y_prob[:, valid], average='macro')\n    else:\n        roc = float('nan')\n    \n    return {\n        'precision_macro': p_m, 'recall_macro': r_m, 'f1_macro': f_m,\n        'precision_weighted': p_w, 'recall_weighted': r_w, 'f1_weighted': f_w,\n        'accuracy': acc, 'roc_auc': roc\n    }\n\ndef plot_roc(y_true, y_prob, class_names):\n    n_cls = y_true.shape[1]\n    # individual curves\n    plt.figure(figsize=(8,6))\n    plt.plot([0,1],[0,1],'k--',alpha=0.5)\n    for i in range(n_cls):\n        if y_true[:,i].sum()==0: continue\n        fpr, tpr, _ = roc_curve(y_true[:,i], y_prob[:,i])\n        auc = roc_auc_score(y_true[:,i], y_prob[:,i])\n        plt.plot(fpr, tpr, linewidth=1, label=f\"{class_names[i]} ({auc:.2f})\")\n    # micro-average\n    fpr_m, tpr_m, _ = roc_curve(y_true.ravel(), y_prob.ravel())\n    auc_m = roc_auc_score(y_true, y_prob, average='micro')\n    plt.plot(fpr_m, tpr_m, color='m', linestyle='--',\n             label=f\"Micro (AUC={auc_m:.2f})\")\n    plt.title(\"ROC Curves\")\n    plt.xlabel(\"FPR\"); plt.ylabel(\"TPR\")\n    plt.legend(fontsize='small',ncol=2)\n    plt.tight_layout(); plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.775270Z","iopub.execute_input":"2025-05-19T12:39:14.775473Z","iopub.status.idle":"2025-05-19T12:39:14.792302Z","shell.execute_reply.started":"2025-05-19T12:39:14.775458Z","shell.execute_reply":"2025-05-19T12:39:14.791580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def mixup(x, y, alpha=Config.mixup_alpha):\n    lam = np.random.beta(alpha, alpha)\n    idx = torch.randperm(x.size(0))\n    x2, y2 = x[idx], y[idx]\n    return lam*x + (1-lam)*x2, lam*y + (1-lam)*y2\n\ndef train_one_epoch(model, loader, opt, scaler):\n    model.train()\n    total_loss = 0.0\n    for x, y in tqdm(loader, desc=\"Train\"):\n        x, y = x.cuda(), y.cuda()\n        # one-hot for BCE\n        y_oh = nn.functional.one_hot(y, Config.num_classes).float()\n        # to multi-label mixing\n        x_mix, y_mix = mixup(x, y_oh)\n        with autocast(enabled=Config.use_amp):\n            logits = model(x_mix)\n            loss   = crit(logits, y_mix)\n        scaler.scale(loss).backward()\n        scaler.unscale_(opt)\n        nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)\n        scaler.step(opt); scaler.update(); opt.zero_grad()\n        total_loss += loss.item()\n    return total_loss / len(loader)\n\ndef validate(model, loader):\n    model.eval()\n    all_probs = []\n    all_true  = []\n    with torch.no_grad():\n        for x, y in tqdm(loader, desc=\"Valid\"):\n            x = x.cuda()\n            logits = model(x)\n            probs  = torch.softmax(logits, dim=1).cpu().numpy()\n            all_probs.append(probs)\n            # one-hot\n            y_oh = nn.functional.one_hot(y, Config.num_classes).numpy()\n            all_true.append(y_oh)\n    y_prob = np.vstack(all_probs)\n    y_true = np.vstack(all_true)\n    return y_true, y_prob","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.793017Z","iopub.execute_input":"2025-05-19T12:39:14.793196Z","iopub.status.idle":"2025-05-19T12:39:14.814810Z","shell.execute_reply.started":"2025-05-19T12:39:14.793182Z","shell.execute_reply":"2025-05-19T12:39:14.814290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=Config.seed)\ndf['fold'] = -1\nfor f,(_,v) in enumerate(skf.split(df, df.primary_label)):\n    df.loc[v,'fold'] = f\n\nmetrics_list = []\nroc_data_list = []\nfor fold in range(5):\n    print(f\"\\n=== Fold {fold} ===\")\n    trn = df[df.fold!=fold]\n    val = df[df.fold==fold]\n    train_loader = DataLoader(BirdClefDataset(trn,'train'),\n                              batch_size=Config.batch_size, shuffle=True,\n                              num_workers=Config.num_workers, pin_memory=True)\n    val_loader   = DataLoader(BirdClefDataset(val,'train'),\n                              batch_size=Config.batch_size, shuffle=False,\n                              num_workers=Config.num_workers, pin_memory=True)\n\n    model = BirdCLEFNet().cuda()\n    crit  = FocalBCE().cuda() \n    opt   = AdamW(model.parameters(), lr=Config.lr_max,\n                  weight_decay=Config.weight_decay)\n    scaler= GradScaler(enabled=Config.use_amp)\n    \n    best_roc = 0.0\n    for ep in range(Config.epochs):\n        loss = train_one_epoch(model, train_loader, opt, scaler)\n        y_true, y_prob = validate(model, val_loader)\n        m = compute_metrics(y_true, y_prob)\n        print(f\"Epoch {ep} | Loss {loss:.4f} | \"\n              f\"ROC {m['roc_auc']:.4f} | Acc {m['accuracy']:.4f} | \"\n              f\"F1_macro {m['f1_macro']:.4f}\")\n        if m['roc_auc'] > best_roc:\n            best_roc = m['roc_auc']\n            torch.save(model.state_dict(), f\"best_fold{fold}.pth\")\n    # final metrics & plot\n    y_true, y_prob = validate(model, val_loader)\n    m = compute_metrics(y_true, y_prob)\n    print(\">> Fold\",fold,\"final metrics:\",m)\n    plot_roc(y_true, y_prob, labels)\n    metrics_list.append(m)\n    roc_data_list.append((y_true, y_prob))\n    del model; gc.collect(); torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-05-19T12:39:14.815434Z","iopub.execute_input":"2025-05-19T12:39:14.815661Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport pandas as pd\nfrom sklearn.metrics import roc_curve, roc_auc_score\n\n# 1) แปลง metrics_list ➔ DataFrame\ndfm = pd.DataFrame(metrics_list, index=[f\"Fold {i+1}\" for i in range(len(metrics_list))])\n# Bar chart เปรียบเทียบ metrics\nax = dfm[['acc','prec_macro','rec_macro','f1_macro','prec_w','rec_w','f1_w']] \\\n    .plot.bar(figsize=(12,6))\nax.set_title('Per-Fold Classification Metrics')\nax.set_ylabel('Score')\nax.legend(bbox_to_anchor=(1.05,1), loc='upper left')\nplt.xticks(rotation=0)\nplt.tight_layout()\nplt.show()\n\n# 2) Overlay micro-average ROC curves per fold\nplt.figure(figsize=(8,6))\nfor i, (y_true, y_prob) in enumerate(roc_data_list):\n    fpr, tpr, _ = roc_curve(y_true.ravel(), y_prob.ravel())\n    auc = roc_auc_score(y_true, y_prob, average='micro')\n    plt.plot(fpr, tpr, label=f'Fold {i+1} (AUC={auc:.3f})')\nplt.plot([0,1],[0,1],'k--',alpha=0.5)\nplt.title('Micro-average ROC Curve per Fold')\nplt.xlabel('False Positive Rate')\nplt.ylabel('True Positive Rate')\nplt.legend(loc='lower right')\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}